@amritk/generate-validators 0.11.7 → 0.11.9
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/LICENSE +21 -0
- package/dist/generators/build-schema.js +21 -44
- package/dist/generators/collect-validator-imports.js +67 -110
- package/dist/generators/generate-files.js +34 -59
- package/dist/generators/generate-validator-function.js +1201 -1800
- package/dist/index.js +4 -1
- package/package.json +6 -6
|
@@ -1,1886 +1,1287 @@
|
|
|
1
|
-
import { escapeRegexPattern } from
|
|
2
|
-
import { getMjstInstanceOf, getMjstPrimitive } from
|
|
3
|
-
import { multipleOfFailExpr, multipleOfPassExpr } from
|
|
4
|
-
import { refToName } from
|
|
5
|
-
import { safeAccessor } from
|
|
6
|
-
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject
|
|
7
|
-
import { unknownKeyCheck } from
|
|
8
|
-
/**
|
|
9
|
-
* Derives the validator function name from a type name.
|
|
10
|
-
* e.g. "InfoObject" → "validateInfoObject"
|
|
11
|
-
*/
|
|
1
|
+
import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
|
|
2
|
+
import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
|
|
3
|
+
import { multipleOfFailExpr, multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
|
|
4
|
+
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
5
|
+
import { safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
6
|
+
import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
7
|
+
import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
|
|
12
8
|
const validatorName = (typeName) => `validate${typeName}`;
|
|
13
|
-
/**
|
|
14
|
-
* Returns the TypeScript typeof string for a JSON Schema primitive type.
|
|
15
|
-
*/
|
|
16
9
|
const typeofString = (type) => {
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
10
|
+
if (type === "integer")
|
|
11
|
+
return "number";
|
|
12
|
+
return type;
|
|
20
13
|
};
|
|
21
|
-
/**
|
|
22
|
-
* Generates the inline condition that is TRUE when `accessor` does NOT equal the
|
|
23
|
-
* `const` value. Primitives compare with `!==`; objects/arrays compare with the
|
|
24
|
-
* runtime `valuesEqual` helper so a reordered-but-equal value still matches (the
|
|
25
|
-
* interpreter uses order-independent deep equality, and `JSON.stringify` would
|
|
26
|
-
* disagree because it is key-order sensitive).
|
|
27
|
-
*/
|
|
28
14
|
const constMismatchCondition = (accessor, value) => {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
15
|
+
if (value === null || typeof value !== "object") {
|
|
16
|
+
return `${accessor} !== ${JSON.stringify(value)}`;
|
|
17
|
+
}
|
|
18
|
+
return `!valuesEqual(${accessor}, ${JSON.stringify(value)})`;
|
|
33
19
|
};
|
|
34
|
-
const SCALAR_ITEM_TYPES = new Set([
|
|
35
|
-
/**
|
|
36
|
-
* True when a schema's values are provably JSON scalars — its `type` is present
|
|
37
|
-
* and every listed type is a primitive. Conservative: a `$ref`, a boolean/absent
|
|
38
|
-
* schema, an `object`/`array` type, or a missing `type` all fail this test.
|
|
39
|
-
*/
|
|
20
|
+
const SCALAR_ITEM_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
|
|
40
21
|
const schemaIsScalarOnly = (schema) => {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
if (!isSchemaObject(schema))
|
|
23
|
+
return false;
|
|
24
|
+
const t = schema["type"];
|
|
25
|
+
if (t === void 0)
|
|
26
|
+
return false;
|
|
27
|
+
const types = Array.isArray(t) ? t : [t];
|
|
28
|
+
return types.length > 0 && types.every((x) => typeof x === "string" && SCALAR_ITEM_TYPES.has(x));
|
|
48
29
|
};
|
|
49
|
-
/**
|
|
50
|
-
* True when an array's elements can only be JSON scalars, so a `uniqueItems`
|
|
51
|
-
* check can dedupe by the cheap `JSON.stringify` projection. When items may be
|
|
52
|
-
* objects or arrays this returns false, and the check must instead compare
|
|
53
|
-
* structurally (the `allUnique` runtime helper): `JSON.stringify` is key-order
|
|
54
|
-
* sensitive and would treat `{ a: 1, b: 2 }` and `{ b: 2, a: 1 }` as distinct,
|
|
55
|
-
* disagreeing with the interpreter's order-independent deep equality.
|
|
56
|
-
*/
|
|
57
30
|
const arrayItemsAreScalarOnly = (schema) => {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
}
|
|
69
|
-
return schemaIsScalarOnly(schema['items']);
|
|
31
|
+
const prefix = schema["prefixItems"];
|
|
32
|
+
if (Array.isArray(prefix)) {
|
|
33
|
+
if (!prefix.every((p) => schemaIsScalarOnly(p)))
|
|
34
|
+
return false;
|
|
35
|
+
const tail = "items" in schema ? schema["items"] : schema["additionalItems"];
|
|
36
|
+
if (tail === false)
|
|
37
|
+
return true;
|
|
38
|
+
return schemaIsScalarOnly(tail);
|
|
39
|
+
}
|
|
40
|
+
return schemaIsScalarOnly(schema["items"]);
|
|
70
41
|
};
|
|
71
|
-
/**
|
|
72
|
-
* Generates the inline condition that is TRUE when a value is the wrong type.
|
|
73
|
-
*/
|
|
74
42
|
const wrongTypeCondition = (accessor, type) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
43
|
+
switch (type) {
|
|
44
|
+
case "string":
|
|
45
|
+
return `typeof ${accessor} !== 'string'`;
|
|
46
|
+
case "number":
|
|
47
|
+
return `typeof ${accessor} !== 'number'`;
|
|
48
|
+
case "integer":
|
|
49
|
+
return `typeof ${accessor} !== 'number' || !Number.isInteger(${accessor})`;
|
|
50
|
+
case "boolean":
|
|
51
|
+
return `typeof ${accessor} !== 'boolean'`;
|
|
52
|
+
case "array":
|
|
53
|
+
return `!Array.isArray(${accessor})`;
|
|
54
|
+
case "null":
|
|
55
|
+
return `${accessor} !== null`;
|
|
56
|
+
case "object":
|
|
57
|
+
return `typeof ${accessor} !== 'object' || ${accessor} === null || Array.isArray(${accessor})`;
|
|
58
|
+
default:
|
|
59
|
+
return "";
|
|
60
|
+
}
|
|
93
61
|
};
|
|
94
|
-
/**
|
|
95
|
-
* Returns the list of type names when a schema's `type` is an array (the JSON
|
|
96
|
-
* Schema multi-type / nullable idiom, e.g. `["string","null"]`), else `null`.
|
|
97
|
-
* `hasType` only recognises a *string* `type`, so without special handling a
|
|
98
|
-
* multi-type schema slips through every branch and emits NO check — not even a
|
|
99
|
-
* required-presence check. A multi-type is validated as the *disjunction* of its
|
|
100
|
-
* per-type checks (the value must match at least one).
|
|
101
|
-
*/
|
|
102
62
|
const getTypeArray = (schema) => {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
63
|
+
if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
|
|
64
|
+
return null;
|
|
65
|
+
return schema.type;
|
|
106
66
|
};
|
|
107
|
-
const createRootContext = () => ({ objVar:
|
|
108
|
-
|
|
109
|
-
* Renders a schema-controlled property name as a static error-path segment.
|
|
110
|
-
*
|
|
111
|
-
* The name is appended to a backtick template-literal path (`` `${_path}/…` ``),
|
|
112
|
-
* so two independent escapings apply. First the JSON Pointer escape (`~`→`~0`,
|
|
113
|
-
* `/`→`~1`, `~` first) so a key containing `/` or `~` reads back unambiguously —
|
|
114
|
-
* matching the paths the runtime-validators interpreter emits. Then a
|
|
115
|
-
* template-literal escape of `` ` ``, `\`, and `$`, so a key like `` a`b `` or
|
|
116
|
-
* `${x}` cannot terminate the literal (a build failure) or inject an
|
|
117
|
-
* interpolation (a runtime `ReferenceError` / arbitrary expression).
|
|
118
|
-
*/
|
|
119
|
-
const pointerSegment = (key) => key
|
|
120
|
-
.replace(/~/g, '~0')
|
|
121
|
-
.replace(/\//g, '~1')
|
|
122
|
-
.replace(/[\\`$]/g, '\\$&');
|
|
123
|
-
/**
|
|
124
|
-
* Returns the `patternProperties` regex sources, or an empty array when the
|
|
125
|
-
* schema declares none. The keys of `patternProperties` are the patterns.
|
|
126
|
-
*/
|
|
67
|
+
const createRootContext = () => ({ objVar: "obj", pathPrefix: "${_path}", depth: 0, hoisted: [] });
|
|
68
|
+
const pointerSegment = (key) => key.replace(/~/g, "~0").replace(/\//g, "~1").replace(/[\\`$]/g, "\\$&");
|
|
127
69
|
const patternPropertySources = (schema) => {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
70
|
+
if (!isSchemaObject(schema) || !("patternProperties" in schema))
|
|
71
|
+
return [];
|
|
72
|
+
const patterns = schema.patternProperties;
|
|
73
|
+
if (typeof patterns !== "object" || patterns === null)
|
|
74
|
+
return [];
|
|
75
|
+
return Object.keys(patterns);
|
|
134
76
|
};
|
|
135
|
-
/**
|
|
136
|
-
* Generates the unknown-key sweep for `additionalProperties: false`, mirroring
|
|
137
|
-
* the interpreter's behaviour (same error message, one error per extra key).
|
|
138
|
-
* The sweep uses `for...in` — the same allocation-free shape Ajv compiles to.
|
|
139
|
-
* The per-key "is this declared" test comes from `unknownKeyCheck`, which
|
|
140
|
-
* inlines `!==` comparisons for small key counts (faster than `Set.has` and
|
|
141
|
-
* allocation-free) and hoists a known-keys `Set` only when the list is long.
|
|
142
|
-
* When the schema also declares `patternProperties`, a key matching any pattern
|
|
143
|
-
* is not "additional": the patterns are compiled once at module scope (the same
|
|
144
|
-
* regex-caching the interpreter does) and a key survives the sweep if it is a
|
|
145
|
-
* known key or matches any pattern.
|
|
146
|
-
*/
|
|
147
77
|
const generateStrictKeyChecks = (schema, ctx) => {
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
` }`,
|
|
171
|
-
];
|
|
78
|
+
if (!isSchemaObject(schema))
|
|
79
|
+
return [];
|
|
80
|
+
if (!hasAdditionalProperties(schema) || schema.additionalProperties !== false)
|
|
81
|
+
return [];
|
|
82
|
+
const known = Object.keys(hasProperties(schema) ? schema.properties : {});
|
|
83
|
+
const d = ctx.depth;
|
|
84
|
+
const check = unknownKeyCheck(known, `_knownKeys${ctx.hoisted.length}`);
|
|
85
|
+
ctx.hoisted.push(...check.declarations);
|
|
86
|
+
const patterns = patternPropertySources(schema);
|
|
87
|
+
let patternGuard = "";
|
|
88
|
+
if (patterns.length > 0) {
|
|
89
|
+
const patternsName = `_patterns${ctx.hoisted.length}`;
|
|
90
|
+
ctx.hoisted.push(`const ${patternsName} = [${patterns.map((p) => `new RegExp(${JSON.stringify(p)})`).join(", ")}]`);
|
|
91
|
+
patternGuard = ` && !${patternsName}.some((re) => re.test(_key${d}))`;
|
|
92
|
+
}
|
|
93
|
+
return [
|
|
94
|
+
` for (const _key${d} in ${ctx.objVar}) {`,
|
|
95
|
+
` if (${check.isUnknown(`_key${d}`)}${patternGuard}) {`,
|
|
96
|
+
` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${_key${d}}\` })`,
|
|
97
|
+
` }`,
|
|
98
|
+
` }`
|
|
99
|
+
];
|
|
172
100
|
};
|
|
173
|
-
/**
|
|
174
|
-
* Emits presence checks for `required` keys that have no `properties` entry.
|
|
175
|
-
* Keys present in `properties` get their missing-property check from
|
|
176
|
-
* {@link generatePropertyChecks}; a required key with no schema of its own would
|
|
177
|
-
* otherwise go unchecked, so its presence is enforced here to match the
|
|
178
|
-
* interpreter and Ajv.
|
|
179
|
-
*/
|
|
180
101
|
const generateMissingRequiredChecks = (schema, ctx) => {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
102
|
+
if (!isSchemaObject(schema) || !hasRequired(schema))
|
|
103
|
+
return [];
|
|
104
|
+
const props = hasProperties(schema) ? schema.properties : {};
|
|
105
|
+
const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
106
|
+
const lines = [];
|
|
107
|
+
for (const key of schema.required) {
|
|
108
|
+
if (Object.hasOwn(props, key))
|
|
109
|
+
continue;
|
|
110
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
111
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
112
|
+
lines.push(` }`);
|
|
113
|
+
}
|
|
114
|
+
return lines;
|
|
194
115
|
};
|
|
195
|
-
/**
|
|
196
|
-
* Generates validation lines for a single property in an object schema.
|
|
197
|
-
* Handles $ref delegation, enum checks, type checks, string/number constraints,
|
|
198
|
-
* and recursion into inline nested objects.
|
|
199
|
-
*/
|
|
200
116
|
const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
];
|
|
239
|
-
if (isRequired) {
|
|
240
|
-
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
241
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
242
|
-
lines.push(` } else {`);
|
|
243
|
-
lines.push(...delegate);
|
|
244
|
-
lines.push(` }`);
|
|
245
|
-
}
|
|
246
|
-
else {
|
|
247
|
-
lines.push(` if (${raw} !== undefined) {`);
|
|
248
|
-
lines.push(...delegate);
|
|
249
|
-
lines.push(` }`);
|
|
250
|
-
}
|
|
251
|
-
return lines;
|
|
252
|
-
}
|
|
253
|
-
// x-mjst instanceOf (e.g. Date) — value must be an instance of the class
|
|
254
|
-
const instanceOf = getMjstInstanceOf(propSchema);
|
|
255
|
-
if (instanceOf) {
|
|
256
|
-
if (isRequired) {
|
|
257
|
-
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
258
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
259
|
-
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
|
|
260
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
261
|
-
lines.push(` }`);
|
|
262
|
-
}
|
|
263
|
-
else {
|
|
264
|
-
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
|
|
265
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
266
|
-
lines.push(` }`);
|
|
267
|
-
}
|
|
268
|
-
return lines;
|
|
269
|
-
}
|
|
270
|
-
// x-mjst primitive (e.g. bigint) — value must satisfy a typeof check
|
|
271
|
-
const primitive = getMjstPrimitive(propSchema);
|
|
272
|
-
if (primitive) {
|
|
273
|
-
if (isRequired) {
|
|
274
|
-
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
275
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
276
|
-
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
|
|
277
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
278
|
-
lines.push(` }`);
|
|
279
|
-
}
|
|
280
|
-
else {
|
|
281
|
-
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
|
|
282
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
283
|
-
lines.push(` }`);
|
|
284
|
-
}
|
|
285
|
-
return lines;
|
|
286
|
-
}
|
|
287
|
-
// const — value must equal the fixed value exactly
|
|
288
|
-
if (hasConst(propSchema)) {
|
|
289
|
-
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
290
|
-
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
291
|
-
if (isRequired) {
|
|
292
|
-
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
293
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
294
|
-
lines.push(` } else if (${mismatch}) {`);
|
|
295
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
296
|
-
lines.push(` }`);
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
|
|
300
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
301
|
-
lines.push(` }`);
|
|
302
|
-
}
|
|
303
|
-
return lines;
|
|
304
|
-
}
|
|
305
|
-
// enum
|
|
306
|
-
if (hasEnum(propSchema)) {
|
|
307
|
-
const allowed = JSON.stringify(propSchema.enum);
|
|
308
|
-
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
309
|
-
if (isRequired) {
|
|
310
|
-
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
311
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
312
|
-
lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`);
|
|
313
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
314
|
-
lines.push(` }`);
|
|
315
|
-
}
|
|
316
|
-
else {
|
|
317
|
-
lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
|
|
318
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
319
|
-
lines.push(` }`);
|
|
320
|
-
}
|
|
321
|
-
return lines;
|
|
322
|
-
}
|
|
323
|
-
// Multi-type / nullable property (array `type`, e.g. `["string","null"]`).
|
|
324
|
-
// `hasType` is false for an array `type`, so without this the property emits no
|
|
325
|
-
// check at all. The value is valid when it matches ANY listed type, i.e. an
|
|
326
|
-
// error is reported only when it is the wrong type for EVERY listed type; the
|
|
327
|
-
// required-presence check is still emitted so a missing required prop fails.
|
|
328
|
-
const typeArray = getTypeArray(propSchema);
|
|
329
|
-
if (typeArray) {
|
|
330
|
-
const allWrong = typeArray
|
|
331
|
-
.map((t) => wrongTypeCondition(raw, t))
|
|
332
|
-
.filter((c) => c !== '')
|
|
333
|
-
.map((c) => `(${c})`)
|
|
334
|
-
.join(' && ');
|
|
335
|
-
const label = typeArray.map((t) => typeofString(t)).join(' or ');
|
|
336
|
-
if (isRequired) {
|
|
337
|
-
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
338
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
339
|
-
if (allWrong) {
|
|
340
|
-
lines.push(` } else if (${allWrong}) {`);
|
|
341
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
342
|
-
}
|
|
343
|
-
lines.push(` }`);
|
|
344
|
-
}
|
|
345
|
-
else if (allWrong) {
|
|
346
|
-
lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
|
|
347
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
348
|
-
lines.push(` }`);
|
|
349
|
-
}
|
|
350
|
-
// Any sibling value constraints (e.g. `minLength` on a `["string","null"]`)
|
|
351
|
-
// still apply — each carries its own runtime-type guard, so it is a no-op for
|
|
352
|
-
// the values it does not target.
|
|
353
|
-
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
354
|
-
return lines;
|
|
117
|
+
if (!isSchemaObject(propSchema)) {
|
|
118
|
+
if (isRequired && propSchema === true) {
|
|
119
|
+
const parentPath2 = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
120
|
+
return [
|
|
121
|
+
` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`,
|
|
122
|
+
` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath2} })`,
|
|
123
|
+
` }`
|
|
124
|
+
];
|
|
125
|
+
}
|
|
126
|
+
return [];
|
|
127
|
+
}
|
|
128
|
+
const raw = `${ctx.objVar}[${JSON.stringify(key)}]`;
|
|
129
|
+
const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
|
|
130
|
+
const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
131
|
+
const lines = [];
|
|
132
|
+
if (hasRef(propSchema)) {
|
|
133
|
+
const ref = propSchema.$ref;
|
|
134
|
+
const vName = validatorName(refToName(ref, suffix));
|
|
135
|
+
const siblings = [
|
|
136
|
+
...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
|
|
137
|
+
...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
|
|
138
|
+
];
|
|
139
|
+
const delegate = [
|
|
140
|
+
` const _r = ${vName}(${raw}, ${path})`,
|
|
141
|
+
` if (_r !== true) errors.push(..._r.errors)`,
|
|
142
|
+
...siblings
|
|
143
|
+
];
|
|
144
|
+
if (isRequired) {
|
|
145
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
146
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
147
|
+
lines.push(` } else {`);
|
|
148
|
+
lines.push(...delegate);
|
|
149
|
+
lines.push(` }`);
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
152
|
+
lines.push(...delegate);
|
|
153
|
+
lines.push(` }`);
|
|
355
154
|
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
else if (wrongType) {
|
|
371
|
-
lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
|
|
372
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
373
|
-
lines.push(` }`);
|
|
374
|
-
}
|
|
375
|
-
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
155
|
+
return lines;
|
|
156
|
+
}
|
|
157
|
+
const instanceOf = getMjstInstanceOf(propSchema);
|
|
158
|
+
if (instanceOf) {
|
|
159
|
+
if (isRequired) {
|
|
160
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
161
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
162
|
+
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
|
|
163
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
164
|
+
lines.push(` }`);
|
|
165
|
+
} else {
|
|
166
|
+
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
|
|
167
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
168
|
+
lines.push(` }`);
|
|
376
169
|
}
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
if (extraLines.length > 0) {
|
|
392
|
-
lines.push(` if (${raw} !== undefined) {`);
|
|
393
|
-
lines.push(...extraLines);
|
|
394
|
-
lines.push(` }`);
|
|
395
|
-
}
|
|
170
|
+
return lines;
|
|
171
|
+
}
|
|
172
|
+
const primitive = getMjstPrimitive(propSchema);
|
|
173
|
+
if (primitive) {
|
|
174
|
+
if (isRequired) {
|
|
175
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
176
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
177
|
+
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
|
|
178
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
179
|
+
lines.push(` }`);
|
|
180
|
+
} else {
|
|
181
|
+
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
|
|
182
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
183
|
+
lines.push(` }`);
|
|
396
184
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
185
|
+
return lines;
|
|
186
|
+
}
|
|
187
|
+
if (hasConst(propSchema)) {
|
|
188
|
+
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
189
|
+
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
190
|
+
if (isRequired) {
|
|
191
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
192
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
193
|
+
lines.push(` } else if (${mismatch}) {`);
|
|
194
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
195
|
+
lines.push(` }`);
|
|
196
|
+
} else {
|
|
197
|
+
lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
|
|
198
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
199
|
+
lines.push(` }`);
|
|
408
200
|
}
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
201
|
+
return lines;
|
|
202
|
+
}
|
|
203
|
+
if (hasEnum(propSchema)) {
|
|
204
|
+
const allowed = JSON.stringify(propSchema.enum);
|
|
205
|
+
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
206
|
+
if (isRequired) {
|
|
207
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
208
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
209
|
+
lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`);
|
|
210
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
211
|
+
lines.push(` }`);
|
|
212
|
+
} else {
|
|
213
|
+
lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
|
|
214
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
215
|
+
lines.push(` }`);
|
|
414
216
|
}
|
|
415
217
|
return lines;
|
|
218
|
+
}
|
|
219
|
+
const typeArray = getTypeArray(propSchema);
|
|
220
|
+
if (typeArray) {
|
|
221
|
+
const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
222
|
+
const label = typeArray.map((t) => typeofString(t)).join(" or ");
|
|
223
|
+
if (isRequired) {
|
|
224
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
225
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
226
|
+
if (allWrong) {
|
|
227
|
+
lines.push(` } else if (${allWrong}) {`);
|
|
228
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
229
|
+
}
|
|
230
|
+
lines.push(` }`);
|
|
231
|
+
} else if (allWrong) {
|
|
232
|
+
lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
|
|
233
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
234
|
+
lines.push(` }`);
|
|
235
|
+
}
|
|
236
|
+
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
237
|
+
return lines;
|
|
238
|
+
}
|
|
239
|
+
if (hasType(propSchema)) {
|
|
240
|
+
const t = propSchema.type;
|
|
241
|
+
const wrongType = wrongTypeCondition(raw, t);
|
|
242
|
+
const typLabel = typeofString(t);
|
|
243
|
+
if (isRequired) {
|
|
244
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
245
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
246
|
+
if (wrongType) {
|
|
247
|
+
lines.push(` } else if (${wrongType}) {`);
|
|
248
|
+
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
249
|
+
}
|
|
250
|
+
lines.push(` }`);
|
|
251
|
+
} else if (wrongType) {
|
|
252
|
+
lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
|
|
253
|
+
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
254
|
+
lines.push(` }`);
|
|
255
|
+
}
|
|
256
|
+
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
257
|
+
}
|
|
258
|
+
const extraLines = hasType(propSchema) ? generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx) : [
|
|
259
|
+
...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
|
|
260
|
+
...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
|
|
261
|
+
];
|
|
262
|
+
if (hasType(propSchema)) {
|
|
263
|
+
if (extraLines.length > 0) {
|
|
264
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
265
|
+
lines.push(...extraLines);
|
|
266
|
+
lines.push(` }`);
|
|
267
|
+
}
|
|
268
|
+
} else if (isRequired) {
|
|
269
|
+
lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
|
|
270
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
271
|
+
if (extraLines.length > 0) {
|
|
272
|
+
lines.push(` } else {`);
|
|
273
|
+
lines.push(...extraLines);
|
|
274
|
+
}
|
|
275
|
+
lines.push(` }`);
|
|
276
|
+
} else if (extraLines.length > 0) {
|
|
277
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
278
|
+
lines.push(...extraLines);
|
|
279
|
+
lines.push(` }`);
|
|
280
|
+
}
|
|
281
|
+
return lines;
|
|
416
282
|
};
|
|
417
|
-
/**
|
|
418
|
-
* Emits the value-shape constraints for a typed value: string (pattern,
|
|
419
|
-
* min/maxLength), number/integer (bounds, multipleOf), typed/`$ref` array items,
|
|
420
|
-
* and recursion into an inline nested object. Shared by the named-property path
|
|
421
|
-
* ({@link generatePropertyChecks}) and the dynamic-key path
|
|
422
|
-
* ({@link generateValueChecks}) so both enforce identical rules. `raw` and `path`
|
|
423
|
-
* are arbitrary expressions, so the same logic serves a static `obj.key` and a
|
|
424
|
-
* `patternProperties` / `additionalProperties` value read at a runtime key.
|
|
425
|
-
*/
|
|
426
283
|
const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
lines.push(` }`);
|
|
552
|
-
}
|
|
553
|
-
if (hasMaxItems(propSchema)) {
|
|
554
|
-
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > ${propSchema.maxItems}) {`);
|
|
555
|
-
lines.push(` errors.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
|
|
556
|
-
lines.push(` }`);
|
|
557
|
-
}
|
|
558
|
-
if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
|
|
559
|
-
const dupCond = arrayItemsAreScalarOnly(sp)
|
|
560
|
-
? `new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length`
|
|
561
|
-
: `!allUnique(${raw} as unknown[])`;
|
|
562
|
-
lines.push(` if (Array.isArray(${raw}) && ${dupCond}) {`);
|
|
563
|
-
lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
|
|
564
|
-
lines.push(` }`);
|
|
565
|
-
}
|
|
566
|
-
// `contains` — at least `minContains` (default 1) and at most `maxContains`
|
|
567
|
-
// items must match the subschema. `minContains: 0` makes any array (even
|
|
568
|
-
// empty) satisfy the lower bound.
|
|
569
|
-
if (isSchemaObject(sp['contains'])) {
|
|
570
|
-
const min = typeof sp['minContains'] === 'number' ? sp['minContains'] : 1;
|
|
571
|
-
const max = typeof sp['maxContains'] === 'number' ? sp['maxContains'] : undefined;
|
|
572
|
-
const matchExpr = generateMatchesExpr('_c', sp['contains'], suffix, ctx);
|
|
573
|
-
const bound = max !== undefined ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
|
|
574
|
-
lines.push(` if (Array.isArray(${raw})) {`);
|
|
575
|
-
lines.push(` const _cn = (${raw} as unknown[]).filter((_c) => ${matchExpr}).length`);
|
|
576
|
-
lines.push(` if (${bound}) {`);
|
|
577
|
-
lines.push(` errors.push({ message: 'array does not contain the required matching items', path: ${path} })`);
|
|
578
|
-
lines.push(` }`);
|
|
579
|
-
lines.push(` }`);
|
|
580
|
-
}
|
|
581
|
-
// Tuple `prefixItems` — each position validated against its own subschema; a
|
|
582
|
-
// sibling `items: false` (or draft `additionalItems: false`) caps the length.
|
|
583
|
-
const prefix = sp['prefixItems'];
|
|
584
|
-
if (Array.isArray(prefix)) {
|
|
585
|
-
lines.push(` if (Array.isArray(${raw})) {`);
|
|
586
|
-
for (let i = 0; i < prefix.length; i++) {
|
|
587
|
-
const itemChecks = generateValueChecks('', `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, prefix[i], suffix, ctx);
|
|
588
|
-
if (itemChecks.length > 0) {
|
|
589
|
-
lines.push(` if (${raw}.length > ${i}) {`);
|
|
590
|
-
lines.push(...itemChecks.map((l) => ` ${l}`));
|
|
591
|
-
lines.push(` }`);
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
if (sp['items'] === false || sp['additionalItems'] === false) {
|
|
595
|
-
lines.push(` if (${raw}.length > ${prefix.length}) {`);
|
|
596
|
-
lines.push(` errors.push({ message: 'must NOT have more than ${prefix.length} items', path: ${path} })`);
|
|
597
|
-
lines.push(` }`);
|
|
598
|
-
}
|
|
599
|
-
lines.push(` }`);
|
|
284
|
+
if (!isSchemaObject(propSchema))
|
|
285
|
+
return [];
|
|
286
|
+
const sp = propSchema;
|
|
287
|
+
const lines = [];
|
|
288
|
+
if (hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema)) {
|
|
289
|
+
if (hasPattern(propSchema)) {
|
|
290
|
+
const re = escapeRegexPattern(propSchema.pattern);
|
|
291
|
+
const msg = JSON.stringify(`must match pattern ${propSchema.pattern}`);
|
|
292
|
+
lines.push(` if (typeof ${raw} === 'string' && !/${re}/.test(${raw})) {`);
|
|
293
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
294
|
+
lines.push(` }`);
|
|
295
|
+
}
|
|
296
|
+
if (hasMinLength(propSchema)) {
|
|
297
|
+
lines.push(` if (typeof ${raw} === 'string' && ${raw}.length < ${propSchema.minLength}) {`);
|
|
298
|
+
lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
|
|
299
|
+
lines.push(` }`);
|
|
300
|
+
}
|
|
301
|
+
if (hasMaxLength(propSchema)) {
|
|
302
|
+
lines.push(` if (typeof ${raw} === 'string' && ${raw}.length > ${propSchema.maxLength}) {`);
|
|
303
|
+
lines.push(` errors.push({ message: 'must have at most ${propSchema.maxLength} characters', path: ${path} })`);
|
|
304
|
+
lines.push(` }`);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema)) {
|
|
308
|
+
if (hasMinimum(propSchema)) {
|
|
309
|
+
const strict = hasStrictExclusiveMinimum(propSchema);
|
|
310
|
+
const op = strict ? "<=" : "<";
|
|
311
|
+
const rel = strict ? ">" : ">=";
|
|
312
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.minimum}) {`);
|
|
313
|
+
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.minimum}', path: ${path} })`);
|
|
314
|
+
lines.push(` }`);
|
|
315
|
+
}
|
|
316
|
+
if (hasMaximum(propSchema)) {
|
|
317
|
+
const strict = hasStrictExclusiveMaximum(propSchema);
|
|
318
|
+
const op = strict ? ">=" : ">";
|
|
319
|
+
const rel = strict ? "<" : "<=";
|
|
320
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} ${op} ${propSchema.maximum}) {`);
|
|
321
|
+
lines.push(` errors.push({ message: 'must be ${rel} ${propSchema.maximum}', path: ${path} })`);
|
|
322
|
+
lines.push(` }`);
|
|
323
|
+
}
|
|
324
|
+
if (hasExclusiveMinimum(propSchema)) {
|
|
325
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} <= ${propSchema.exclusiveMinimum}) {`);
|
|
326
|
+
lines.push(` errors.push({ message: 'must be > ${propSchema.exclusiveMinimum}', path: ${path} })`);
|
|
327
|
+
lines.push(` }`);
|
|
328
|
+
}
|
|
329
|
+
if (hasExclusiveMaximum(propSchema)) {
|
|
330
|
+
lines.push(` if (typeof ${raw} === 'number' && ${raw} >= ${propSchema.exclusiveMaximum}) {`);
|
|
331
|
+
lines.push(` errors.push({ message: 'must be < ${propSchema.exclusiveMaximum}', path: ${path} })`);
|
|
332
|
+
lines.push(` }`);
|
|
333
|
+
}
|
|
334
|
+
if (hasMultipleOf(propSchema)) {
|
|
335
|
+
lines.push(` if (typeof ${raw} === 'number' && ${multipleOfFailExpr(raw, propSchema.multipleOf)}) {`);
|
|
336
|
+
lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
|
|
337
|
+
lines.push(` }`);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (hasItems(propSchema)) {
|
|
341
|
+
const itemSchema = propSchema.items;
|
|
342
|
+
const iv = `_i${ctx.depth}`;
|
|
343
|
+
const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
|
|
344
|
+
if (hasRef(itemSchema)) {
|
|
345
|
+
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
346
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
347
|
+
lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
348
|
+
lines.push(` const _ir = ${vName}(${raw}[${iv}], ${itemPath})`);
|
|
349
|
+
lines.push(` if (_ir !== true) errors.push(..._ir.errors)`);
|
|
350
|
+
lines.push(` }`);
|
|
351
|
+
lines.push(` }`);
|
|
352
|
+
} else if (isSchemaObject(itemSchema)) {
|
|
353
|
+
const itemVar = `_item${ctx.depth}`;
|
|
354
|
+
const detail = generateValueChecks("", itemVar, itemPath, itemSchema, suffix, ctx, true);
|
|
355
|
+
if (detail.length > 0) {
|
|
356
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
357
|
+
lines.push(` for (let ${iv} = 0; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
358
|
+
lines.push(` const ${itemVar} = ${raw}[${iv}]`);
|
|
359
|
+
lines.push(...detail.map((l) => ` ${l}`));
|
|
360
|
+
lines.push(` }`);
|
|
361
|
+
lines.push(` }`);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || isSchemaObject(sp["contains"]) || Array.isArray(sp["prefixItems"]) || sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
|
|
366
|
+
if (sp["items"] === false && !Array.isArray(sp["prefixItems"])) {
|
|
367
|
+
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
|
|
368
|
+
lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
|
|
369
|
+
lines.push(` }`);
|
|
370
|
+
}
|
|
371
|
+
if (hasMinItems(propSchema)) {
|
|
372
|
+
lines.push(` if (Array.isArray(${raw}) && ${raw}.length < ${propSchema.minItems}) {`);
|
|
373
|
+
lines.push(` errors.push({ message: 'must have at least ${propSchema.minItems} items', path: ${path} })`);
|
|
374
|
+
lines.push(` }`);
|
|
375
|
+
}
|
|
376
|
+
if (hasMaxItems(propSchema)) {
|
|
377
|
+
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > ${propSchema.maxItems}) {`);
|
|
378
|
+
lines.push(` errors.push({ message: 'must have at most ${propSchema.maxItems} items', path: ${path} })`);
|
|
379
|
+
lines.push(` }`);
|
|
380
|
+
}
|
|
381
|
+
if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
|
|
382
|
+
const dupCond = arrayItemsAreScalarOnly(sp) ? `new Set((${raw} as unknown[]).map((_u) => JSON.stringify(_u))).size !== ${raw}.length` : `!allUnique(${raw} as unknown[])`;
|
|
383
|
+
lines.push(` if (Array.isArray(${raw}) && ${dupCond}) {`);
|
|
384
|
+
lines.push(` errors.push({ message: 'must NOT have duplicate items', path: ${path} })`);
|
|
385
|
+
lines.push(` }`);
|
|
386
|
+
}
|
|
387
|
+
if (isSchemaObject(sp["contains"])) {
|
|
388
|
+
const min = typeof sp["minContains"] === "number" ? sp["minContains"] : 1;
|
|
389
|
+
const max = typeof sp["maxContains"] === "number" ? sp["maxContains"] : void 0;
|
|
390
|
+
const matchExpr = generateMatchesExpr("_c", sp["contains"], suffix, ctx);
|
|
391
|
+
const bound = max !== void 0 ? `_cn < ${min} || _cn > ${max}` : `_cn < ${min}`;
|
|
392
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
393
|
+
lines.push(` const _cn = (${raw} as unknown[]).filter((_c) => ${matchExpr}).length`);
|
|
394
|
+
lines.push(` if (${bound}) {`);
|
|
395
|
+
lines.push(` errors.push({ message: 'array does not contain the required matching items', path: ${path} })`);
|
|
396
|
+
lines.push(` }`);
|
|
397
|
+
lines.push(` }`);
|
|
398
|
+
}
|
|
399
|
+
const prefix = sp["prefixItems"];
|
|
400
|
+
if (Array.isArray(prefix)) {
|
|
401
|
+
lines.push(` if (Array.isArray(${raw})) {`);
|
|
402
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
403
|
+
const itemChecks = generateValueChecks("", `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, prefix[i], suffix, ctx);
|
|
404
|
+
if (itemChecks.length > 0) {
|
|
405
|
+
lines.push(` if (${raw}.length > ${i}) {`);
|
|
406
|
+
lines.push(...itemChecks.map((l) => ` ${l}`));
|
|
407
|
+
lines.push(` }`);
|
|
600
408
|
}
|
|
409
|
+
}
|
|
410
|
+
if (sp["items"] === false || sp["additionalItems"] === false) {
|
|
411
|
+
lines.push(` if (${raw}.length > ${prefix.length}) {`);
|
|
412
|
+
lines.push(` errors.push({ message: 'must NOT have more than ${prefix.length} items', path: ${path} })`);
|
|
413
|
+
lines.push(` }`);
|
|
414
|
+
}
|
|
415
|
+
lines.push(` }`);
|
|
601
416
|
}
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
// runtime check, so this is a no-op for non-object schemas.
|
|
606
|
-
lines.push(...generateInlineObjectChecks(key, propSchema, raw, suffix, ctx));
|
|
607
|
-
return lines;
|
|
417
|
+
}
|
|
418
|
+
lines.push(...generateInlineObjectChecks(key, propSchema, raw, suffix, ctx));
|
|
419
|
+
return lines;
|
|
608
420
|
};
|
|
609
|
-
/**
|
|
610
|
-
* Validates a value located at a *dynamic* key (a `patternProperties` or
|
|
611
|
-
* `additionalProperties` value), an array item, a combinator branch, or a
|
|
612
|
-
* `dependentSchemas` subschema against `propSchema`. `raw` and `path` are
|
|
613
|
-
* caller-supplied expressions (e.g. `obj[_k]` and `` `${_path}/${_k}` ``) so the
|
|
614
|
-
* checks read a runtime location. By default the leaf checks are
|
|
615
|
-
* `!== undefined`-guarded (an absent optional value is valid); pass
|
|
616
|
-
* `required = true` for values that must be present (array items — a sparse hole
|
|
617
|
-
* reads as `undefined` and must fail), which drops that guard. `_key` is unused
|
|
618
|
-
* (the location is fully encoded by `path`) but kept for positional-call parity
|
|
619
|
-
* with the combinator generators.
|
|
620
|
-
*/
|
|
621
421
|
const generateValueChecks = (_key, raw, path, propSchema, suffix, ctx, required = false) => {
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
else {
|
|
637
|
-
lines.push(` if (${raw} !== undefined) {`);
|
|
638
|
-
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
639
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
640
|
-
lines.push(` }`);
|
|
641
|
-
}
|
|
642
|
-
return lines;
|
|
422
|
+
if (!isSchemaObject(propSchema))
|
|
423
|
+
return [];
|
|
424
|
+
const lines = [];
|
|
425
|
+
const presence = required ? "" : `${raw} !== undefined && `;
|
|
426
|
+
if (hasRef(propSchema)) {
|
|
427
|
+
const vName = validatorName(refToName(propSchema.$ref, suffix));
|
|
428
|
+
if (required) {
|
|
429
|
+
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
430
|
+
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(` if (${raw} !== undefined) {`);
|
|
433
|
+
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
434
|
+
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
435
|
+
lines.push(` }`);
|
|
643
436
|
}
|
|
644
|
-
const instanceOf = getMjstInstanceOf(propSchema);
|
|
645
|
-
if (instanceOf) {
|
|
646
|
-
lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
|
|
647
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
648
|
-
lines.push(` }`);
|
|
649
|
-
return lines;
|
|
650
|
-
}
|
|
651
|
-
const primitive = getMjstPrimitive(propSchema);
|
|
652
|
-
if (primitive) {
|
|
653
|
-
lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
|
|
654
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
655
|
-
lines.push(` }`);
|
|
656
|
-
return lines;
|
|
657
|
-
}
|
|
658
|
-
if (hasConst(propSchema)) {
|
|
659
|
-
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
660
|
-
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
661
|
-
lines.push(` if (${presence}${mismatch}) {`);
|
|
662
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
663
|
-
lines.push(` }`);
|
|
664
|
-
return lines;
|
|
665
|
-
}
|
|
666
|
-
if (hasEnum(propSchema)) {
|
|
667
|
-
const allowed = JSON.stringify(propSchema.enum);
|
|
668
|
-
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
|
|
669
|
-
lines.push(` if (${presence}!(${allowed} as unknown[]).includes(${raw})) {`);
|
|
670
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
671
|
-
lines.push(` }`);
|
|
672
|
-
return lines;
|
|
673
|
-
}
|
|
674
|
-
if (hasType(propSchema)) {
|
|
675
|
-
const t = propSchema.type;
|
|
676
|
-
const wrongType = wrongTypeCondition(raw, t);
|
|
677
|
-
const typLabel = typeofString(t);
|
|
678
|
-
if (wrongType) {
|
|
679
|
-
lines.push(` if (${presence}(${wrongType})) {`);
|
|
680
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
681
|
-
lines.push(` }`);
|
|
682
|
-
}
|
|
683
|
-
}
|
|
684
|
-
// Constraint and combinator checks run regardless of a declared `type`: they
|
|
685
|
-
// gate on keyword presence + a runtime-type guard, so a type-less subschema
|
|
686
|
-
// (a combinator branch like `{ required: [...] }` or `{ minItems: 2 }`) is
|
|
687
|
-
// still validated rather than collapsing to "matches everything".
|
|
688
|
-
//
|
|
689
|
-
// This value lives at `path` (a template literal), so anchor the recursion's
|
|
690
|
-
// context there and one nesting level deeper: any nested object/array it emits
|
|
691
|
-
// then builds paths relative to THIS value and mints collision-free variable
|
|
692
|
-
// names, independent of the caller's context. `key` is intentionally dropped
|
|
693
|
-
// (set to `''`) because `path` already locates the value.
|
|
694
|
-
const valueCtx = {
|
|
695
|
-
objVar: ctx.objVar,
|
|
696
|
-
pathPrefix: path.slice(1, -1),
|
|
697
|
-
depth: ctx.depth + 1,
|
|
698
|
-
hoisted: ctx.hoisted,
|
|
699
|
-
};
|
|
700
|
-
lines.push(...generateConstraintChecks('', raw, path, propSchema, suffix, valueCtx));
|
|
701
|
-
lines.push(...generateCombinatorChecks('', raw, path, propSchema, suffix, valueCtx));
|
|
702
437
|
return lines;
|
|
438
|
+
}
|
|
439
|
+
const instanceOf = getMjstInstanceOf(propSchema);
|
|
440
|
+
if (instanceOf) {
|
|
441
|
+
lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
|
|
442
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
443
|
+
lines.push(` }`);
|
|
444
|
+
return lines;
|
|
445
|
+
}
|
|
446
|
+
const primitive = getMjstPrimitive(propSchema);
|
|
447
|
+
if (primitive) {
|
|
448
|
+
lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
|
|
449
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
450
|
+
lines.push(` }`);
|
|
451
|
+
return lines;
|
|
452
|
+
}
|
|
453
|
+
if (hasConst(propSchema)) {
|
|
454
|
+
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
455
|
+
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
456
|
+
lines.push(` if (${presence}${mismatch}) {`);
|
|
457
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
458
|
+
lines.push(` }`);
|
|
459
|
+
return lines;
|
|
460
|
+
}
|
|
461
|
+
if (hasEnum(propSchema)) {
|
|
462
|
+
const allowed = JSON.stringify(propSchema.enum);
|
|
463
|
+
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
464
|
+
lines.push(` if (${presence}!(${allowed} as unknown[]).includes(${raw})) {`);
|
|
465
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
466
|
+
lines.push(` }`);
|
|
467
|
+
return lines;
|
|
468
|
+
}
|
|
469
|
+
if (hasType(propSchema)) {
|
|
470
|
+
const t = propSchema.type;
|
|
471
|
+
const wrongType = wrongTypeCondition(raw, t);
|
|
472
|
+
const typLabel = typeofString(t);
|
|
473
|
+
if (wrongType) {
|
|
474
|
+
lines.push(` if (${presence}(${wrongType})) {`);
|
|
475
|
+
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
476
|
+
lines.push(` }`);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const valueCtx = {
|
|
480
|
+
objVar: ctx.objVar,
|
|
481
|
+
pathPrefix: path.slice(1, -1),
|
|
482
|
+
depth: ctx.depth + 1,
|
|
483
|
+
hoisted: ctx.hoisted
|
|
484
|
+
};
|
|
485
|
+
lines.push(...generateConstraintChecks("", raw, path, propSchema, suffix, valueCtx));
|
|
486
|
+
lines.push(...generateCombinatorChecks("", raw, path, propSchema, suffix, valueCtx));
|
|
487
|
+
return lines;
|
|
703
488
|
};
|
|
704
|
-
/**
|
|
705
|
-
* A boolean expression that is `true` when `raw` matches `sub`. Reuses the value
|
|
706
|
-
* checks but collects their errors into a throwaway local buffer, so the same
|
|
707
|
-
* logic that produces error messages also answers the yes/no question the
|
|
708
|
-
* combinators (`anyOf`/`oneOf`/`not`/`if`) and `contains` need.
|
|
709
|
-
*/
|
|
710
489
|
const generateMatchesExpr = (raw, sub, suffix, ctx) => {
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
return `((): boolean => { const _m: ValidationError[] = []\n${body}\n return _m.length === 0 })()`;
|
|
490
|
+
if (sub === true)
|
|
491
|
+
return "true";
|
|
492
|
+
if (sub === false)
|
|
493
|
+
return "false";
|
|
494
|
+
if (!isSchemaObject(sub))
|
|
495
|
+
return "true";
|
|
496
|
+
const checks = generateValueChecks("", raw, "`${_path}`", sub, suffix, ctx);
|
|
497
|
+
if (checks.length === 0)
|
|
498
|
+
return "true";
|
|
499
|
+
const body = checks.join("\n").replaceAll("errors.push(", "_m.push(");
|
|
500
|
+
return `((): boolean => { const _m: ValidationError[] = []
|
|
501
|
+
${body}
|
|
502
|
+
return _m.length === 0 })()`;
|
|
725
503
|
};
|
|
726
|
-
/**
|
|
727
|
-
* Emits the combinator keywords (`allOf`, `anyOf`, `oneOf`, `not`,
|
|
728
|
-
* `if`/`then`/`else`). `allOf` surfaces each branch's errors directly; the others
|
|
729
|
-
* evaluate branch membership as a boolean via {@link generateMatchesExpr}.
|
|
730
|
-
*/
|
|
731
504
|
const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
505
|
+
if (!isSchemaObject(schema))
|
|
506
|
+
return [];
|
|
507
|
+
const lines = [];
|
|
508
|
+
if (hasAllOf(schema)) {
|
|
509
|
+
for (const branch of schema.allOf)
|
|
510
|
+
lines.push(...generateValueChecks(key, raw, path, branch, suffix, ctx));
|
|
511
|
+
}
|
|
512
|
+
if (hasAnyOf(schema) && schema.anyOf.length > 0) {
|
|
513
|
+
const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
|
|
514
|
+
lines.push(` if (!(${conds.join(" || ")})) {`);
|
|
515
|
+
lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
|
|
516
|
+
lines.push(` }`);
|
|
517
|
+
}
|
|
518
|
+
if (hasOneOf(schema) && schema.oneOf.length > 0) {
|
|
519
|
+
const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
|
|
520
|
+
lines.push(` if ((${conds.join(" + ")}) !== 1) {`);
|
|
521
|
+
lines.push(` errors.push({ message: 'must match exactly one schema in oneOf', path: ${path} })`);
|
|
522
|
+
lines.push(` }`);
|
|
523
|
+
}
|
|
524
|
+
const not = schema["not"];
|
|
525
|
+
if (not !== void 0 && (isSchemaObject(not) || typeof not === "boolean")) {
|
|
526
|
+
const cond = generateMatchesExpr(raw, not, suffix, ctx);
|
|
527
|
+
lines.push(` if (${cond}) {`);
|
|
528
|
+
lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
|
|
529
|
+
lines.push(` }`);
|
|
530
|
+
}
|
|
531
|
+
const ifSchema = schema["if"];
|
|
532
|
+
if (ifSchema !== void 0 && (isSchemaObject(ifSchema) || typeof ifSchema === "boolean")) {
|
|
533
|
+
const thenSchema = schema["then"];
|
|
534
|
+
const elseSchema = schema["else"];
|
|
535
|
+
const thenLines = thenSchema !== void 0 ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
|
|
536
|
+
const elseLines = elseSchema !== void 0 ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
|
|
537
|
+
if (thenLines.length > 0 || elseLines.length > 0) {
|
|
538
|
+
lines.push(` if (${generateMatchesExpr(raw, ifSchema, suffix, ctx)}) {`);
|
|
539
|
+
lines.push(...thenLines);
|
|
540
|
+
lines.push(` } else {`);
|
|
541
|
+
lines.push(...elseLines);
|
|
542
|
+
lines.push(` }`);
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
return lines;
|
|
773
546
|
};
|
|
774
|
-
/**
|
|
775
|
-
* Emits validation for `patternProperties` and a schema-form
|
|
776
|
-
* `additionalProperties` (the `false` form is handled by
|
|
777
|
-
* {@link generateStrictKeyChecks}). For each object key, every matching
|
|
778
|
-
* `patternProperties` subschema runs against the value; keys reached by neither
|
|
779
|
-
* `properties` nor any pattern fall through to `additionalProperties`. This
|
|
780
|
-
* mirrors the runtime interpreter, which validates these values rather than only
|
|
781
|
-
* gating extra keys.
|
|
782
|
-
*/
|
|
783
547
|
const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
if (
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
if (
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
lines.push(` }`);
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
return lines;
|
|
548
|
+
if (!isSchemaObject(schema))
|
|
549
|
+
return [];
|
|
550
|
+
const obj = ctx.objVar;
|
|
551
|
+
const d = ctx.depth;
|
|
552
|
+
const lines = [];
|
|
553
|
+
const patternsRecord = "patternProperties" in schema && typeof schema.patternProperties === "object" && schema.patternProperties !== null ? schema.patternProperties : {};
|
|
554
|
+
const patternEntries = Object.entries(patternsRecord);
|
|
555
|
+
for (const [pattern, sub] of patternEntries) {
|
|
556
|
+
const re = escapeRegexPattern(pattern);
|
|
557
|
+
const kv = `_pk${d}`;
|
|
558
|
+
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
|
|
559
|
+
if (valueChecks.length === 0)
|
|
560
|
+
continue;
|
|
561
|
+
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
562
|
+
lines.push(` if (/${re}/.test(${kv})) {`);
|
|
563
|
+
lines.push(...valueChecks.map((line) => ` ${line}`));
|
|
564
|
+
lines.push(` }`);
|
|
565
|
+
lines.push(` }`);
|
|
566
|
+
}
|
|
567
|
+
if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
|
|
568
|
+
const additional = schema.additionalProperties;
|
|
569
|
+
const kv = `_ak${d}`;
|
|
570
|
+
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, additional, suffix, ctx);
|
|
571
|
+
if (valueChecks.length > 0) {
|
|
572
|
+
const known = Object.keys(hasProperties(schema) ? schema.properties : {});
|
|
573
|
+
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
574
|
+
if (known.length > 0)
|
|
575
|
+
lines.push(` if (${JSON.stringify(known)}.includes(${kv})) continue`);
|
|
576
|
+
for (const pattern of Object.keys(patternsRecord)) {
|
|
577
|
+
lines.push(` if (/${escapeRegexPattern(pattern)}/.test(${kv})) continue`);
|
|
578
|
+
}
|
|
579
|
+
lines.push(...valueChecks.map((line) => ` ${line}`));
|
|
580
|
+
lines.push(` }`);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
return lines;
|
|
824
584
|
};
|
|
825
|
-
/**
|
|
826
|
-
* Generates the recursive checks for an inline nested object property, i.e. an
|
|
827
|
-
* object schema written directly under `properties` rather than referenced via
|
|
828
|
-
* `$ref` (those delegate to the referenced validator instead). The value is
|
|
829
|
-
* narrowed into its own block-scoped variable and each nested property runs
|
|
830
|
-
* through the same per-property generator, so nesting works to any depth.
|
|
831
|
-
*/
|
|
832
585
|
const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
` }`,
|
|
869
|
-
];
|
|
586
|
+
if (!isSchemaObject(propSchema))
|
|
587
|
+
return [];
|
|
588
|
+
const child = {
|
|
589
|
+
objVar: `_obj${ctx.depth + 1}`,
|
|
590
|
+
// When `key` is empty the value is located AT `ctx.pathPrefix` already (e.g. an
|
|
591
|
+
// inline object reached through a combinator branch or a dynamic-key value), so
|
|
592
|
+
// appending `/${key}` would emit a spurious `//` or trailing `/` in error paths.
|
|
593
|
+
pathPrefix: key === "" ? ctx.pathPrefix : `${ctx.pathPrefix}/${pointerSegment(key)}`,
|
|
594
|
+
depth: ctx.depth + 1,
|
|
595
|
+
hoisted: ctx.hoisted
|
|
596
|
+
};
|
|
597
|
+
const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
|
|
598
|
+
const properties = hasProperties(propSchema) ? propSchema.properties : {};
|
|
599
|
+
const innerLines = [];
|
|
600
|
+
for (const [childKey, childSchema] of Object.entries(properties)) {
|
|
601
|
+
innerLines.push(...generatePropertyChecks(childKey, childSchema, required.has(childKey), suffix, child));
|
|
602
|
+
}
|
|
603
|
+
innerLines.push(...generateMissingRequiredChecks(propSchema, child));
|
|
604
|
+
innerLines.push(...generatePatternAndAdditionalChecks(propSchema, suffix, child));
|
|
605
|
+
innerLines.push(...generateStrictKeyChecks(propSchema, child));
|
|
606
|
+
innerLines.push(...generateDependentRequiredChecks(propSchema, child));
|
|
607
|
+
innerLines.push(...generateDependentSchemasChecks(propSchema, suffix, child));
|
|
608
|
+
innerLines.push(...generateDependenciesChecks(propSchema, suffix, child));
|
|
609
|
+
innerLines.push(...generateMinMaxPropertiesChecks(propSchema, child));
|
|
610
|
+
if (hasPropertyNames(propSchema) && isSchemaObject(propSchema.propertyNames)) {
|
|
611
|
+
innerLines.push(...generatePropertyNameChecks(propSchema.propertyNames, suffix, child));
|
|
612
|
+
}
|
|
613
|
+
if (innerLines.length === 0)
|
|
614
|
+
return [];
|
|
615
|
+
return [
|
|
616
|
+
` if (typeof ${raw} === 'object' && ${raw} !== null && !Array.isArray(${raw})) {`,
|
|
617
|
+
` const ${child.objVar} = ${raw} as Record<string, unknown>`,
|
|
618
|
+
...innerLines.map((line) => ` ${line}`),
|
|
619
|
+
` }`
|
|
620
|
+
];
|
|
870
621
|
};
|
|
871
|
-
/**
|
|
872
|
-
* Generates the `propertyNames` loop: every object key is a string, and the
|
|
873
|
-
* *whole* subschema is validated against each key — not just the
|
|
874
|
-
* `pattern`/length/`enum`/`const`/`$ref` subset. Delegating to
|
|
875
|
-
* {@link generateValueChecks} keeps the generator in lockstep with the
|
|
876
|
-
* interpreter, which runs `matchesSchema(nameSchema, key)` per key, so a
|
|
877
|
-
* subschema carrying a combinator, `type`, `multipleOf`, etc. is enforced too.
|
|
878
|
-
* A key is always a present string, so the value checks run in `required` mode
|
|
879
|
-
* (no `!== undefined` guard).
|
|
880
|
-
*/
|
|
881
622
|
const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
623
|
+
if (!isSchemaObject(nameSchema))
|
|
624
|
+
return [];
|
|
625
|
+
const at = `\`${ctx.pathPrefix}/\${_name}\``;
|
|
626
|
+
const nameCtx = {
|
|
627
|
+
objVar: ctx.objVar,
|
|
628
|
+
pathPrefix: `${ctx.pathPrefix}/\${_name}`,
|
|
629
|
+
depth: ctx.depth + 1,
|
|
630
|
+
hoisted: ctx.hoisted
|
|
631
|
+
};
|
|
632
|
+
const checks = generateValueChecks("", "_name", at, nameSchema, suffix, nameCtx, true);
|
|
633
|
+
if (checks.length === 0)
|
|
634
|
+
return [];
|
|
635
|
+
return [` for (const _name of Object.keys(${ctx.objVar})) {`, ...checks.map((line) => ` ${line}`), ` }`];
|
|
895
636
|
};
|
|
896
|
-
/**
|
|
897
|
-
* Emits `dependentRequired` checks: when a trigger key is present, each of its
|
|
898
|
-
* declared dependencies must be present too. Reads the object and reports at the
|
|
899
|
-
* current node via `ctx`, so it works at the root and inside nested objects.
|
|
900
|
-
*/
|
|
901
637
|
const generateDependentRequiredChecks = (schema, ctx) => {
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
638
|
+
if (!isSchemaObject(schema) || !hasDependentRequired(schema))
|
|
639
|
+
return [];
|
|
640
|
+
const obj = ctx.objVar;
|
|
641
|
+
const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
642
|
+
const lines = [];
|
|
643
|
+
for (const [trigger, deps] of Object.entries(schema.dependentRequired)) {
|
|
644
|
+
if (!Array.isArray(deps))
|
|
645
|
+
continue;
|
|
646
|
+
for (const dep of deps) {
|
|
647
|
+
const msg = JSON.stringify(`must have property '${dep}' when '${trigger}' is present`);
|
|
648
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) {`);
|
|
649
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
650
|
+
lines.push(` }`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return lines;
|
|
918
654
|
};
|
|
919
|
-
/**
|
|
920
|
-
* Emits `dependentSchemas` checks (2020-12): when a trigger property is present,
|
|
921
|
-
* the *whole object* must also match the associated subschema. Mirrors the
|
|
922
|
-
* interpreter, which applies the subschema in place against the object. A `true`
|
|
923
|
-
* subschema permits everything (no-op); a `false` subschema makes the trigger's
|
|
924
|
-
* presence always invalid.
|
|
925
|
-
*/
|
|
926
655
|
const generateDependentSchemasChecks = (schema, suffix, ctx) => {
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
}
|
|
946
|
-
if (!isSchemaObject(sub))
|
|
947
|
-
continue;
|
|
948
|
-
// The subschema applies to the object itself, so validate the current object
|
|
949
|
-
// variable against it and gate the whole block on the trigger's presence.
|
|
950
|
-
const checks = generateValueChecks('', obj, objPath, sub, suffix, ctx);
|
|
951
|
-
if (checks.length === 0)
|
|
952
|
-
continue;
|
|
953
|
-
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
954
|
-
lines.push(...checks.map((line) => ` ${line}`));
|
|
955
|
-
lines.push(` }`);
|
|
656
|
+
if (!isSchemaObject(schema))
|
|
657
|
+
return [];
|
|
658
|
+
const dep = schema["dependentSchemas"];
|
|
659
|
+
if (typeof dep !== "object" || dep === null || Array.isArray(dep))
|
|
660
|
+
return [];
|
|
661
|
+
const obj = ctx.objVar;
|
|
662
|
+
const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
663
|
+
const objPath = `\`${ctx.pathPrefix}\``;
|
|
664
|
+
const lines = [];
|
|
665
|
+
for (const [trigger, sub] of Object.entries(dep)) {
|
|
666
|
+
if (sub === true)
|
|
667
|
+
continue;
|
|
668
|
+
if (sub === false) {
|
|
669
|
+
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
670
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
671
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
672
|
+
lines.push(` }`);
|
|
673
|
+
continue;
|
|
956
674
|
}
|
|
957
|
-
|
|
675
|
+
if (!isSchemaObject(sub))
|
|
676
|
+
continue;
|
|
677
|
+
const checks = generateValueChecks("", obj, objPath, sub, suffix, ctx);
|
|
678
|
+
if (checks.length === 0)
|
|
679
|
+
continue;
|
|
680
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
681
|
+
lines.push(...checks.map((line) => ` ${line}`));
|
|
682
|
+
lines.push(` }`);
|
|
683
|
+
}
|
|
684
|
+
return lines;
|
|
958
685
|
};
|
|
959
|
-
/**
|
|
960
|
-
* Emits draft-07 `dependencies` — the dual-form predecessor of
|
|
961
|
-
* `dependentRequired` + `dependentSchemas`. When a trigger property is present,
|
|
962
|
-
* an array value requires each listed key, and a schema value is applied to the
|
|
963
|
-
* *whole object*. Mirrors the interpreter, which branches on the value's shape.
|
|
964
|
-
* A `false` subschema makes the trigger's mere presence invalid; a `true`
|
|
965
|
-
* subschema is a no-op.
|
|
966
|
-
*/
|
|
967
686
|
const generateDependenciesChecks = (schema, suffix, ctx) => {
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
986
|
-
lines.push(` }`);
|
|
987
|
-
}
|
|
988
|
-
continue;
|
|
989
|
-
}
|
|
990
|
-
// Schema form: the subschema applies to the object itself.
|
|
991
|
-
if (value === true)
|
|
992
|
-
continue;
|
|
993
|
-
if (value === false) {
|
|
994
|
-
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
995
|
-
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
996
|
-
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
997
|
-
lines.push(` }`);
|
|
998
|
-
continue;
|
|
999
|
-
}
|
|
1000
|
-
if (!isSchemaObject(value))
|
|
1001
|
-
continue;
|
|
1002
|
-
const checks = generateValueChecks('', obj, objPath, value, suffix, ctx);
|
|
1003
|
-
if (checks.length === 0)
|
|
1004
|
-
continue;
|
|
1005
|
-
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
1006
|
-
lines.push(...checks.map((line) => ` ${line}`));
|
|
687
|
+
if (!isSchemaObject(schema))
|
|
688
|
+
return [];
|
|
689
|
+
const dep = schema["dependencies"];
|
|
690
|
+
if (typeof dep !== "object" || dep === null || Array.isArray(dep))
|
|
691
|
+
return [];
|
|
692
|
+
const obj = ctx.objVar;
|
|
693
|
+
const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
694
|
+
const objPath = `\`${ctx.pathPrefix}\``;
|
|
695
|
+
const lines = [];
|
|
696
|
+
for (const [trigger, value] of Object.entries(dep)) {
|
|
697
|
+
if (Array.isArray(value)) {
|
|
698
|
+
for (const key of value) {
|
|
699
|
+
if (typeof key !== "string")
|
|
700
|
+
continue;
|
|
701
|
+
const msg = JSON.stringify(`must have property '${key}' when '${trigger}' is present`);
|
|
702
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(key)} in ${obj})) {`);
|
|
703
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
1007
704
|
lines.push(` }`);
|
|
1008
|
-
|
|
1009
|
-
|
|
705
|
+
}
|
|
706
|
+
continue;
|
|
707
|
+
}
|
|
708
|
+
if (value === true)
|
|
709
|
+
continue;
|
|
710
|
+
if (value === false) {
|
|
711
|
+
const msg = JSON.stringify(`must NOT have property '${trigger}'`);
|
|
712
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
713
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
714
|
+
lines.push(` }`);
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
if (!isSchemaObject(value))
|
|
718
|
+
continue;
|
|
719
|
+
const checks = generateValueChecks("", obj, objPath, value, suffix, ctx);
|
|
720
|
+
if (checks.length === 0)
|
|
721
|
+
continue;
|
|
722
|
+
lines.push(` if (${JSON.stringify(trigger)} in ${obj}) {`);
|
|
723
|
+
lines.push(...checks.map((line) => ` ${line}`));
|
|
724
|
+
lines.push(` }`);
|
|
725
|
+
}
|
|
726
|
+
return lines;
|
|
1010
727
|
};
|
|
1011
|
-
/**
|
|
1012
|
-
* Emits `minProperties` / `maxProperties` bounds on the object's key count,
|
|
1013
|
-
* mirroring the interpreter (which counts the object's own enumerable keys and
|
|
1014
|
-
* reports at the object node). Counting once into a depth-scoped local keeps the
|
|
1015
|
-
* two bounds from re-walking the keys.
|
|
1016
|
-
*/
|
|
1017
728
|
const generateMinMaxPropertiesChecks = (schema, ctx) => {
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
729
|
+
if (!isSchemaObject(schema))
|
|
730
|
+
return [];
|
|
731
|
+
const hasMin = hasMinProperties(schema);
|
|
732
|
+
const hasMax = hasMaxProperties(schema);
|
|
733
|
+
if (!hasMin && !hasMax)
|
|
734
|
+
return [];
|
|
735
|
+
const obj = ctx.objVar;
|
|
736
|
+
const at = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
737
|
+
const count = `_pc${ctx.depth}`;
|
|
738
|
+
const lines = [` const ${count} = Object.keys(${obj}).length`];
|
|
739
|
+
if (hasMin) {
|
|
740
|
+
const msg = JSON.stringify(`must have at least ${schema.minProperties} properties`);
|
|
741
|
+
lines.push(` if (${count} < ${schema.minProperties}) {`);
|
|
742
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
743
|
+
lines.push(` }`);
|
|
744
|
+
}
|
|
745
|
+
if (hasMax) {
|
|
746
|
+
const msg = JSON.stringify(`must have at most ${schema.maxProperties} properties`);
|
|
747
|
+
lines.push(` if (${count} > ${schema.maxProperties}) {`);
|
|
748
|
+
lines.push(` errors.push({ message: ${msg}, path: ${at} })`);
|
|
749
|
+
lines.push(` }`);
|
|
750
|
+
}
|
|
751
|
+
return lines;
|
|
1041
752
|
};
|
|
1042
|
-
/**
|
|
1043
|
-
* Builds the `&&` conditions that prove a single property is valid, or `null`
|
|
1044
|
-
* when the property carries any keyword the slow path enforces beyond a bare
|
|
1045
|
-
* type check (pattern, min/max, enum, const, `$ref`, items, x-mjst, …). A `null`
|
|
1046
|
-
* makes the whole guard bail so that input still flows through the slow,
|
|
1047
|
-
* error-collecting path — the guard only ever returns true for *provably* valid
|
|
1048
|
-
* input, never weakening a verdict. `objAcc` is the expression yielding the
|
|
1049
|
-
* parent object (already narrowed to a record); `key` indexes into it.
|
|
1050
|
-
*/
|
|
1051
|
-
/**
|
|
1052
|
-
* Whether an object schema carries a combinator keyword (`allOf`, `anyOf`,
|
|
1053
|
-
* `oneOf`, `not`, `if`) that the error-collecting slow path enforces but neither
|
|
1054
|
-
* flat guard can mirror. When present, both guards must bail so their early
|
|
1055
|
-
* `return true` never accepts a document the combinator would reject.
|
|
1056
|
-
*/
|
|
1057
753
|
const hasObjectLevelCombinator = (schema) => {
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
754
|
+
if (!isSchemaObject(schema))
|
|
755
|
+
return false;
|
|
756
|
+
return hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema;
|
|
1061
757
|
};
|
|
1062
758
|
const guardPropConditions = (key, propSchema, objAcc) => {
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
}
|
|
1090
|
-
if (!hasType(propSchema))
|
|
1091
|
-
return null;
|
|
1092
|
-
switch (propSchema.type) {
|
|
1093
|
-
case 'string':
|
|
1094
|
-
return [`typeof ${raw} === 'string'`];
|
|
1095
|
-
case 'number':
|
|
1096
|
-
return [`typeof ${raw} === 'number'`];
|
|
1097
|
-
case 'integer':
|
|
1098
|
-
return [`typeof ${raw} === 'number'`, `Number.isInteger(${raw})`];
|
|
1099
|
-
case 'boolean':
|
|
1100
|
-
return [`typeof ${raw} === 'boolean'`];
|
|
1101
|
-
case 'null':
|
|
1102
|
-
return [`${raw} === null`];
|
|
1103
|
-
case 'object':
|
|
1104
|
-
// Member access into the nested record is only reached after the shape
|
|
1105
|
-
// check ahead of it in the `&&` chain, so the cast is always safe.
|
|
1106
|
-
return guardObjectConditions(propSchema, raw, `(${raw} as Record<string, unknown>)`);
|
|
1107
|
-
// Arrays need a per-item loop the guard can't express, and any other type
|
|
1108
|
-
// (null, multi-type, untyped) is left to the slow path.
|
|
1109
|
-
default:
|
|
1110
|
-
return null;
|
|
1111
|
-
}
|
|
759
|
+
if (!isSchemaObject(propSchema))
|
|
760
|
+
return null;
|
|
761
|
+
const raw = safeAccessor(objAcc, key);
|
|
762
|
+
if (hasRef(propSchema) || hasEnum(propSchema) || hasConst(propSchema) || hasOneOf(propSchema) || hasAnyOf(propSchema) || hasAllOf(propSchema) || "not" in propSchema || "if" in propSchema || getMjstInstanceOf(propSchema) !== void 0 || getMjstPrimitive(propSchema) !== void 0 || hasPattern(propSchema) || hasMinLength(propSchema) || hasMaxLength(propSchema) || hasMinimum(propSchema) || hasMaximum(propSchema) || hasExclusiveMinimum(propSchema) || hasExclusiveMaximum(propSchema) || hasMultipleOf(propSchema) || hasItems(propSchema)) {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
if (!hasType(propSchema))
|
|
766
|
+
return null;
|
|
767
|
+
switch (propSchema.type) {
|
|
768
|
+
case "string":
|
|
769
|
+
return [`typeof ${raw} === 'string'`];
|
|
770
|
+
case "number":
|
|
771
|
+
return [`typeof ${raw} === 'number'`];
|
|
772
|
+
case "integer":
|
|
773
|
+
return [`typeof ${raw} === 'number'`, `Number.isInteger(${raw})`];
|
|
774
|
+
case "boolean":
|
|
775
|
+
return [`typeof ${raw} === 'boolean'`];
|
|
776
|
+
case "null":
|
|
777
|
+
return [`${raw} === null`];
|
|
778
|
+
case "object":
|
|
779
|
+
return guardObjectConditions(propSchema, raw, `(${raw} as Record<string, unknown>)`);
|
|
780
|
+
// Arrays need a per-item loop the guard can't express, and any other type
|
|
781
|
+
// (null, multi-type, untyped) is left to the slow path.
|
|
782
|
+
default:
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
1112
785
|
};
|
|
1113
|
-
/** A property key an array carries with a non-`undefined` value: `length`, or a
|
|
1114
|
-
* canonical array index. A required prop on one of these can't be used to rule
|
|
1115
|
-
* out arrays (an array's `length` is a number, an index can be anything). */
|
|
1116
786
|
const ARRAY_INDEX_KEY = /^(0|[1-9]\d*)$/;
|
|
1117
|
-
|
|
1118
|
-
const TYPEOF_CHECKABLE_TYPES = new Set(['string', 'number', 'integer', 'boolean', 'object']);
|
|
1119
|
-
/**
|
|
1120
|
-
* Whether some required, typeof-guarded property proves the value can't be an
|
|
1121
|
-
* array — letting the object shape-check drop its `!Array.isArray(...)` term. An
|
|
1122
|
-
* array indexed by a normal key yields `undefined` (or an inherited method),
|
|
1123
|
-
* which no `typeof === 'string' | 'number' | 'boolean' | 'object'` accepts, so
|
|
1124
|
-
* that field check already rejects arrays. Keys an array does carry a real value
|
|
1125
|
-
* for (`length`, numeric indices) are excluded, since those could slip through.
|
|
1126
|
-
*/
|
|
787
|
+
const TYPEOF_CHECKABLE_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "object"]);
|
|
1127
788
|
const arrayRejectedByRequiredProp = (keys, required, properties) => {
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
}
|
|
1138
|
-
}
|
|
1139
|
-
return false;
|
|
789
|
+
for (const key of keys) {
|
|
790
|
+
if (!required.has(key) || key === "length" || ARRAY_INDEX_KEY.test(key))
|
|
791
|
+
continue;
|
|
792
|
+
const propSchema = properties[key];
|
|
793
|
+
if (propSchema !== void 0 && isSchemaObject(propSchema) && hasType(propSchema) && TYPEOF_CHECKABLE_TYPES.has(propSchema.type)) {
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
return false;
|
|
1140
798
|
};
|
|
1141
|
-
/**
|
|
1142
|
-
* Builds the allocation-free boolean guard for an object schema as a list of
|
|
1143
|
-
* `&&` conditions, or `null` when the schema can't be proven valid by a cheap
|
|
1144
|
-
* expression. The conditions are ordered so every member access is guarded by
|
|
1145
|
-
* the object-shape check that precedes it in the `&&` chain.
|
|
1146
|
-
*
|
|
1147
|
-
* The guard only handles the happy path: every declared property must be
|
|
1148
|
-
* required and a bare-typed scalar or a likewise-guardable nested object. Any
|
|
1149
|
-
* optional property, object-level constraint the slow path enforces
|
|
1150
|
-
* (`patternProperties`, `propertyNames`, `dependentRequired`, an
|
|
1151
|
-
* `additionalProperties` *schema*), or unguardable property makes it bail, and
|
|
1152
|
-
* the validator falls back to its full error-collecting body.
|
|
1153
|
-
*/
|
|
1154
799
|
const guardObjectConditions = (schema, raw, objAcc) => {
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
const propConditions = guardPropConditions(key, properties[key], objAcc);
|
|
1196
|
-
if (propConditions === null)
|
|
1197
|
-
return null;
|
|
1198
|
-
conditions.push(...propConditions);
|
|
1199
|
-
}
|
|
1200
|
-
if (strict) {
|
|
1201
|
-
// `additionalProperties: false` with every declared property required: once
|
|
1202
|
-
// the typeof checks confirm each key is present, an exact key count proves
|
|
1203
|
-
// there are no extras — TypeBox's trick, with no loop and no Set.
|
|
1204
|
-
if (!keys.every((key) => required.has(key)))
|
|
1205
|
-
return null;
|
|
1206
|
-
conditions.push(`Object.keys(${objAcc}).length === ${keys.length}`);
|
|
1207
|
-
}
|
|
1208
|
-
return conditions;
|
|
800
|
+
if (!isObjectSchema(schema))
|
|
801
|
+
return null;
|
|
802
|
+
if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
|
|
803
|
+
return null;
|
|
804
|
+
if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
|
|
805
|
+
return null;
|
|
806
|
+
if (isSchemaObject(schema) && "patternProperties" in schema)
|
|
807
|
+
return null;
|
|
808
|
+
if (hasObjectLevelCombinator(schema))
|
|
809
|
+
return null;
|
|
810
|
+
let strict = false;
|
|
811
|
+
if (hasAdditionalProperties(schema)) {
|
|
812
|
+
if (schema.additionalProperties === false)
|
|
813
|
+
strict = true;
|
|
814
|
+
else
|
|
815
|
+
return null;
|
|
816
|
+
}
|
|
817
|
+
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
818
|
+
const properties = hasProperties(schema) ? schema.properties : {};
|
|
819
|
+
const keys = Object.keys(properties);
|
|
820
|
+
for (const key of required) {
|
|
821
|
+
if (!Object.hasOwn(properties, key))
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? "" : ` && !Array.isArray(${raw})`;
|
|
825
|
+
const conditions = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
|
|
826
|
+
for (const key of keys) {
|
|
827
|
+
if (!required.has(key))
|
|
828
|
+
return null;
|
|
829
|
+
const propConditions = guardPropConditions(key, properties[key], objAcc);
|
|
830
|
+
if (propConditions === null)
|
|
831
|
+
return null;
|
|
832
|
+
conditions.push(...propConditions);
|
|
833
|
+
}
|
|
834
|
+
if (strict) {
|
|
835
|
+
if (!keys.every((key) => required.has(key)))
|
|
836
|
+
return null;
|
|
837
|
+
conditions.push(`Object.keys(${objAcc}).length === ${keys.length}`);
|
|
838
|
+
}
|
|
839
|
+
return conditions;
|
|
1209
840
|
};
|
|
1210
|
-
/**
|
|
1211
|
-
* Generates a validator function body for an object schema, checking each
|
|
1212
|
-
* property's presence and type and collecting all errors.
|
|
1213
|
-
*/
|
|
1214
841
|
const generateObjectValidator = (schema, typeName, suffix) => {
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
propertyLines.push(...
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
if (
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
const collectBody = (name, exported) => [
|
|
1272
|
-
`${exported ? 'export ' : ''}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1273
|
-
` const obj = input as Record<string, unknown>`,
|
|
1274
|
-
` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
|
|
1275
|
-
` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
|
|
1276
|
-
` }`,
|
|
1277
|
-
``,
|
|
1278
|
-
` let errors: ValidationError[] | undefined`,
|
|
1279
|
-
body,
|
|
1280
|
-
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1281
|
-
`}`,
|
|
1282
|
-
].join('\n');
|
|
1283
|
-
// No guard: the exported validator is the error-collecting function itself.
|
|
1284
|
-
if (!guard) {
|
|
1285
|
-
return `${hoistedBlock}${collectBody(vName, true)}`;
|
|
1286
|
-
}
|
|
1287
|
-
// With a guard, keep the happy path inside the exported function — the guard
|
|
1288
|
-
// is inlined as an early `return true`, so a valid input never pays an extra
|
|
1289
|
-
// call — and move only the cold, error-collecting body into a separate
|
|
1290
|
-
// (unexported) function. That keeps `validateX` itself tiny (guard + a single
|
|
1291
|
-
// tail call) so V8 optimises it well, without the giant error body bloating
|
|
1292
|
-
// the hot path. The exported `(input, _path?) => ValidationResult` contract
|
|
1293
|
-
// is unchanged.
|
|
1294
|
-
const collectName = `${vName}Errors`;
|
|
1295
|
-
return [
|
|
1296
|
-
`${hoistedBlock}${collectBody(collectName, false)}`,
|
|
1297
|
-
``,
|
|
1298
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1299
|
-
` const obj = input as Record<string, unknown>`,
|
|
1300
|
-
` if (`,
|
|
1301
|
-
guard.map((condition) => ` ${condition}`).join(' &&\n'),
|
|
1302
|
-
` ) {`,
|
|
1303
|
-
` return true`,
|
|
1304
|
-
` }`,
|
|
1305
|
-
` return ${collectName}(input, _path)`,
|
|
1306
|
-
`}`,
|
|
1307
|
-
].join('\n');
|
|
842
|
+
const vName = validatorName(typeName);
|
|
843
|
+
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
844
|
+
const properties = hasProperties(schema) ? schema.properties : {};
|
|
845
|
+
const ctx = createRootContext();
|
|
846
|
+
const propertyLines = [];
|
|
847
|
+
for (const [key, propSchema] of Object.entries(properties)) {
|
|
848
|
+
const checks = generatePropertyChecks(key, propSchema, required.has(key), suffix, ctx);
|
|
849
|
+
if (checks.length > 0) {
|
|
850
|
+
propertyLines.push(...checks);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
propertyLines.push(...generateMissingRequiredChecks(schema, ctx));
|
|
854
|
+
propertyLines.push(...generatePatternAndAdditionalChecks(schema, suffix, ctx));
|
|
855
|
+
propertyLines.push(...generateStrictKeyChecks(schema, ctx));
|
|
856
|
+
propertyLines.push(...generateDependentRequiredChecks(schema, ctx));
|
|
857
|
+
propertyLines.push(...generateDependentSchemasChecks(schema, suffix, ctx));
|
|
858
|
+
propertyLines.push(...generateDependenciesChecks(schema, suffix, ctx));
|
|
859
|
+
propertyLines.push(...generateMinMaxPropertiesChecks(schema, ctx));
|
|
860
|
+
if (hasPropertyNames(schema) && isSchemaObject(schema.propertyNames)) {
|
|
861
|
+
propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
|
|
862
|
+
}
|
|
863
|
+
propertyLines.push(...generateCombinatorChecks("", "obj", "`${_path}`", schema, suffix, ctx));
|
|
864
|
+
const body = (propertyLines.length > 0 ? "\n" + propertyLines.join("\n") + "\n" : "").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
865
|
+
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
|
|
866
|
+
|
|
867
|
+
` : "";
|
|
868
|
+
const guard = guardObjectConditions(schema, "input", "obj");
|
|
869
|
+
const collectBody = (name, exported) => [
|
|
870
|
+
`${exported ? "export " : ""}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
871
|
+
` const obj = input as Record<string, unknown>`,
|
|
872
|
+
` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
|
|
873
|
+
` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
|
|
874
|
+
` }`,
|
|
875
|
+
``,
|
|
876
|
+
` let errors: ValidationError[] | undefined`,
|
|
877
|
+
body,
|
|
878
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
879
|
+
`}`
|
|
880
|
+
].join("\n");
|
|
881
|
+
if (!guard) {
|
|
882
|
+
return `${hoistedBlock}${collectBody(vName, true)}`;
|
|
883
|
+
}
|
|
884
|
+
const collectName = `${vName}Errors`;
|
|
885
|
+
return [
|
|
886
|
+
`${hoistedBlock}${collectBody(collectName, false)}`,
|
|
887
|
+
``,
|
|
888
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
889
|
+
` const obj = input as Record<string, unknown>`,
|
|
890
|
+
` if (`,
|
|
891
|
+
guard.map((condition) => ` ${condition}`).join(" &&\n"),
|
|
892
|
+
` ) {`,
|
|
893
|
+
` return true`,
|
|
894
|
+
` }`,
|
|
895
|
+
` return ${collectName}(input, _path)`,
|
|
896
|
+
`}`
|
|
897
|
+
].join("\n");
|
|
1308
898
|
};
|
|
1309
|
-
/**
|
|
1310
|
-
* Derives the boolean type-guard name from a type name.
|
|
1311
|
-
* e.g. "InfoObject" → "isInfoObject"
|
|
1312
|
-
*/
|
|
1313
899
|
const guardName = (typeName) => `is${typeName}`;
|
|
1314
|
-
/**
|
|
1315
|
-
* Builds the membership test for an `enum`, matching the slow path's
|
|
1316
|
-
* `[...].includes(value)` verdict exactly. For the common all-primitive case it
|
|
1317
|
-
* emits a parenthesized `a === x || a === y` chain — no per-call array
|
|
1318
|
-
* allocation and no linear scan, so it stays on the allocation-free hot path —
|
|
1319
|
-
* and falls back to `.includes` when a member is an object/array (reference
|
|
1320
|
-
* equality) or `NaN` (where `includes`'s SameValueZero differs from `===`).
|
|
1321
|
-
*/
|
|
1322
900
|
const enumMembershipExpr = (values, acc) => {
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
}
|
|
1329
|
-
return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
|
|
901
|
+
const allPrimitive = values.length > 0 && values.every((v) => (v === null || typeof v !== "object") && typeof v !== "function") && !values.some((v) => typeof v === "number" && Number.isNaN(v));
|
|
902
|
+
if (allPrimitive) {
|
|
903
|
+
return `(${values.map((v) => `${acc} === ${JSON.stringify(v)}`).join(" || ")})`;
|
|
904
|
+
}
|
|
905
|
+
return `(${JSON.stringify(values)} as unknown[]).includes(${acc})`;
|
|
1330
906
|
};
|
|
1331
|
-
/**
|
|
1332
|
-
* Builds a boolean expression that is TRUE iff `acc` satisfies `schema`, with the
|
|
1333
|
-
* *exact same verdict* as the error-collecting validator — or `null` when the
|
|
1334
|
-
* schema carries something the flat form can't faithfully mirror ($ref, unions,
|
|
1335
|
-
* `const`, x-mjst, etc.), in which case the whole guard falls back to calling the
|
|
1336
|
-
* validator. Used for a property value or an array item; `acc` is the expression
|
|
1337
|
-
* yielding the value.
|
|
1338
|
-
*/
|
|
1339
907
|
const booleanLeafExpr = (schema, acc) => {
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
case 'boolean':
|
|
1397
|
-
return `typeof ${acc} === 'boolean'`;
|
|
1398
|
-
case 'null':
|
|
1399
|
-
return `${acc} === null`;
|
|
1400
|
-
case 'object': {
|
|
1401
|
-
const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
|
|
1402
|
-
return parts === null ? null : parts.join(' && ');
|
|
1403
|
-
}
|
|
1404
|
-
case 'array':
|
|
1405
|
-
return booleanArrayExpr(schema, acc);
|
|
1406
|
-
default:
|
|
1407
|
-
return null;
|
|
1408
|
-
}
|
|
908
|
+
if (!isSchemaObject(schema))
|
|
909
|
+
return null;
|
|
910
|
+
if (hasRef(schema) || hasConst(schema) || hasOneOf(schema) || "anyOf" in schema || "allOf" in schema || "not" in schema || "if" in schema || "contains" in schema || "prefixItems" in schema || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
if (hasEnum(schema)) {
|
|
914
|
+
return enumMembershipExpr(schema.enum, acc);
|
|
915
|
+
}
|
|
916
|
+
if (!hasType(schema))
|
|
917
|
+
return null;
|
|
918
|
+
const t = schema.type;
|
|
919
|
+
switch (t) {
|
|
920
|
+
// Each constraint is the exact negation of the validator's error condition
|
|
921
|
+
// (`!(len < min)`, not `len >= min`) so edge values — most importantly `NaN`,
|
|
922
|
+
// which the validator accepts for a constrained number since `NaN < min` is
|
|
923
|
+
// false — get the identical verdict.
|
|
924
|
+
case "string": {
|
|
925
|
+
const parts = [`typeof ${acc} === 'string'`];
|
|
926
|
+
if (hasPattern(schema))
|
|
927
|
+
parts.push(`/${escapeRegexPattern(schema.pattern)}/.test(${acc})`);
|
|
928
|
+
if (hasMinLength(schema))
|
|
929
|
+
parts.push(`!(${acc}.length < ${schema.minLength})`);
|
|
930
|
+
if (hasMaxLength(schema))
|
|
931
|
+
parts.push(`!(${acc}.length > ${schema.maxLength})`);
|
|
932
|
+
return parts.join(" && ");
|
|
933
|
+
}
|
|
934
|
+
case "number":
|
|
935
|
+
case "integer": {
|
|
936
|
+
const parts = [`typeof ${acc} === 'number'`];
|
|
937
|
+
if (t === "integer")
|
|
938
|
+
parts.push(`Number.isInteger(${acc})`);
|
|
939
|
+
if (hasMinimum(schema))
|
|
940
|
+
parts.push(`!(${acc} ${hasStrictExclusiveMinimum(schema) ? "<=" : "<"} ${schema.minimum})`);
|
|
941
|
+
if (hasMaximum(schema))
|
|
942
|
+
parts.push(`!(${acc} ${hasStrictExclusiveMaximum(schema) ? ">=" : ">"} ${schema.maximum})`);
|
|
943
|
+
if (hasExclusiveMinimum(schema))
|
|
944
|
+
parts.push(`!(${acc} <= ${schema.exclusiveMinimum})`);
|
|
945
|
+
if (hasExclusiveMaximum(schema))
|
|
946
|
+
parts.push(`!(${acc} >= ${schema.exclusiveMaximum})`);
|
|
947
|
+
if (hasMultipleOf(schema))
|
|
948
|
+
parts.push(multipleOfPassExpr(acc, schema.multipleOf));
|
|
949
|
+
return parts.join(" && ");
|
|
950
|
+
}
|
|
951
|
+
case "boolean":
|
|
952
|
+
return `typeof ${acc} === 'boolean'`;
|
|
953
|
+
case "null":
|
|
954
|
+
return `${acc} === null`;
|
|
955
|
+
case "object": {
|
|
956
|
+
const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`);
|
|
957
|
+
return parts === null ? null : parts.join(" && ");
|
|
958
|
+
}
|
|
959
|
+
case "array":
|
|
960
|
+
return booleanArrayExpr(schema, acc);
|
|
961
|
+
default:
|
|
962
|
+
return null;
|
|
963
|
+
}
|
|
1409
964
|
};
|
|
1410
|
-
/**
|
|
1411
|
-
* Boolean expression for an array value. Mirrors the validator: array shape,
|
|
1412
|
-
* `minItems`/`maxItems`/`uniqueItems`, and each item validated in full via
|
|
1413
|
-
* {@link booleanLeafExpr}. Returns `null` for `$ref` items, or when an item schema
|
|
1414
|
-
* can't be expressed flat, so the whole guard defers to the validator.
|
|
1415
|
-
*
|
|
1416
|
-
* Item iteration goes through `Array.from` rather than `Array.prototype.every`
|
|
1417
|
-
* because `every` *skips holes* in a sparse array (`[, 'x']`), whereas the
|
|
1418
|
-
* validator's index-based `for` loop reads a hole as `undefined` and rejects it.
|
|
1419
|
-
* Materialising the array first makes the guard's verdict match the slow path's
|
|
1420
|
-
* on sparse input — the guard must never accept what the slow path would reject.
|
|
1421
|
-
*/
|
|
1422
965
|
const booleanArrayExpr = (schema, acc) => {
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
if (hasRef(items))
|
|
1444
|
-
return null;
|
|
1445
|
-
// Validate each item in full, mirroring the validator's per-item checks so the
|
|
1446
|
-
// guard reaches the identical verdict. `booleanLeafExpr` returns `null` for item
|
|
1447
|
-
// schemas it can't express flat — bail so the validator decides, keeping the
|
|
1448
|
-
// guard from ever accepting what the slow path would reject.
|
|
1449
|
-
const itemExpr = booleanLeafExpr(items, '_it');
|
|
1450
|
-
if (itemExpr === null)
|
|
1451
|
-
return null;
|
|
1452
|
-
return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
|
|
966
|
+
const parts = [`Array.isArray(${acc})`];
|
|
967
|
+
if (hasMinItems(schema))
|
|
968
|
+
parts.push(`${acc}.length >= ${schema.minItems}`);
|
|
969
|
+
if (hasMaxItems(schema))
|
|
970
|
+
parts.push(`${acc}.length <= ${schema.maxItems}`);
|
|
971
|
+
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
972
|
+
parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${acc}.length` : `allUnique(${acc} as unknown[])`);
|
|
973
|
+
}
|
|
974
|
+
const base = parts.join(" && ");
|
|
975
|
+
if (!hasItems(schema))
|
|
976
|
+
return base;
|
|
977
|
+
const items = schema.items;
|
|
978
|
+
if (!isSchemaObject(items))
|
|
979
|
+
return base;
|
|
980
|
+
if (hasRef(items))
|
|
981
|
+
return null;
|
|
982
|
+
const itemExpr = booleanLeafExpr(items, "_it");
|
|
983
|
+
if (itemExpr === null)
|
|
984
|
+
return null;
|
|
985
|
+
return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
|
|
1453
986
|
};
|
|
1454
|
-
/**
|
|
1455
|
-
* Builds the `&&` conditions proving an object value is valid (same verdict as
|
|
1456
|
-
* the error-collecting validator), or `null` when any property or object-level
|
|
1457
|
-
* keyword can't be mirrored flat. `raw` yields the value (for the shape check);
|
|
1458
|
-
* `objAcc` is the same value narrowed to a record (for member access).
|
|
1459
|
-
*/
|
|
1460
987
|
const booleanObjectParts = (schema, raw, objAcc) => {
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
const
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
// uses. Kept when no such property exists.
|
|
1490
|
-
const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? '' : ` && !Array.isArray(${raw})`;
|
|
1491
|
-
const parts = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
|
|
1492
|
-
for (const key of keys) {
|
|
1493
|
-
const propSchema = properties[key];
|
|
1494
|
-
if (propSchema === undefined || !isSchemaObject(propSchema))
|
|
1495
|
-
return null;
|
|
1496
|
-
const member = safeAccessor(objAcc, key);
|
|
1497
|
-
const expr = booleanLeafExpr(propSchema, member);
|
|
1498
|
-
if (expr === null)
|
|
1499
|
-
return null;
|
|
1500
|
-
parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
|
|
1501
|
-
}
|
|
1502
|
-
if (strict) {
|
|
1503
|
-
// `additionalProperties: false`: with every property required, an exact key
|
|
1504
|
-
// count proves no extras (the typeof checks above already proved presence);
|
|
1505
|
-
// otherwise sweep the keys against the declared set.
|
|
1506
|
-
if (keys.length === 0) {
|
|
1507
|
-
parts.push(`Object.keys(${objAcc}).length === 0`);
|
|
1508
|
-
}
|
|
1509
|
-
else if (keys.every((key) => required.has(key))) {
|
|
1510
|
-
parts.push(`Object.keys(${objAcc}).length === ${keys.length}`);
|
|
1511
|
-
}
|
|
1512
|
-
else {
|
|
1513
|
-
const known = keys.map((key) => `_k === ${JSON.stringify(key)}`).join(' || ');
|
|
1514
|
-
parts.push(`Object.keys(${objAcc}).every((_k) => ${known})`);
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
return parts;
|
|
1518
|
-
};
|
|
1519
|
-
/**
|
|
1520
|
-
* Generates the exported boolean type-guard `isTypeName(input): input is TypeName`.
|
|
1521
|
-
*
|
|
1522
|
-
* Unlike `validateTypeName` (which returns rich `ValidationResult` errors), this
|
|
1523
|
-
* is a single flat boolean predicate — no error array, no cold-path call — so V8
|
|
1524
|
-
* inlines it like a hand-written `check`, matching the shape of TypeBox's
|
|
1525
|
-
* compiled checker. It returns the *same verdict* as the validator. When the
|
|
1526
|
-
* schema carries anything the flat form can't mirror exactly, it falls back to
|
|
1527
|
-
* `validateTypeName(input) === true`, which is always correct.
|
|
1528
|
-
*/
|
|
1529
|
-
export const generateBooleanGuard = (schema, typeName, _suffix = '') => {
|
|
1530
|
-
const name = guardName(typeName);
|
|
1531
|
-
const fallback = `export const ${name} = (input: unknown): input is ${typeName} => ${validatorName(typeName)}(input) === true`;
|
|
1532
|
-
// Fold `nullable: true` into `anyOf` so the guard's verdict matches the
|
|
1533
|
-
// validator's (which applies the same rewrite). Without this a nullable node's
|
|
1534
|
-
// guard would reject `null` while the validator accepts it.
|
|
1535
|
-
const rewritten = rewriteNullable(schema);
|
|
1536
|
-
if (isObjectSchema(rewritten)) {
|
|
1537
|
-
const parts = booleanObjectParts(rewritten, 'input', 'obj');
|
|
1538
|
-
if (parts === null)
|
|
1539
|
-
return fallback;
|
|
1540
|
-
return [
|
|
1541
|
-
`export const ${name} = (input: unknown): input is ${typeName} => {`,
|
|
1542
|
-
` const obj = input as Record<string, unknown>`,
|
|
1543
|
-
` return (`,
|
|
1544
|
-
parts.map((part) => ` ${part}`).join(' &&\n'),
|
|
1545
|
-
` )`,
|
|
1546
|
-
`}`,
|
|
1547
|
-
].join('\n');
|
|
1548
|
-
}
|
|
1549
|
-
// Non-object roots (scalar, enum, array) can often be expressed inline too.
|
|
1550
|
-
const expr = booleanLeafExpr(rewritten, 'input');
|
|
988
|
+
if (!isObjectSchema(schema))
|
|
989
|
+
return null;
|
|
990
|
+
if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
|
|
991
|
+
return null;
|
|
992
|
+
if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
|
|
993
|
+
return null;
|
|
994
|
+
if (isSchemaObject(schema) && "patternProperties" in schema)
|
|
995
|
+
return null;
|
|
996
|
+
if (hasObjectLevelCombinator(schema))
|
|
997
|
+
return null;
|
|
998
|
+
let strict = false;
|
|
999
|
+
if (hasAdditionalProperties(schema)) {
|
|
1000
|
+
if (schema.additionalProperties === false)
|
|
1001
|
+
strict = true;
|
|
1002
|
+
else
|
|
1003
|
+
return null;
|
|
1004
|
+
}
|
|
1005
|
+
const required = new Set(hasRequired(schema) ? schema.required : []);
|
|
1006
|
+
const properties = hasProperties(schema) ? schema.properties : {};
|
|
1007
|
+
const keys = Object.keys(properties);
|
|
1008
|
+
const arrayCheck = arrayRejectedByRequiredProp(keys, required, properties) ? "" : ` && !Array.isArray(${raw})`;
|
|
1009
|
+
const parts = [`typeof ${raw} === 'object' && ${raw} !== null${arrayCheck}`];
|
|
1010
|
+
for (const key of keys) {
|
|
1011
|
+
const propSchema = properties[key];
|
|
1012
|
+
if (propSchema === void 0 || !isSchemaObject(propSchema))
|
|
1013
|
+
return null;
|
|
1014
|
+
const member = safeAccessor(objAcc, key);
|
|
1015
|
+
const expr = booleanLeafExpr(propSchema, member);
|
|
1551
1016
|
if (expr === null)
|
|
1552
|
-
|
|
1553
|
-
|
|
1017
|
+
return null;
|
|
1018
|
+
parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
|
|
1019
|
+
}
|
|
1020
|
+
if (strict) {
|
|
1021
|
+
if (keys.length === 0) {
|
|
1022
|
+
parts.push(`Object.keys(${objAcc}).length === 0`);
|
|
1023
|
+
} else if (keys.every((key) => required.has(key))) {
|
|
1024
|
+
parts.push(`Object.keys(${objAcc}).length === ${keys.length}`);
|
|
1025
|
+
} else {
|
|
1026
|
+
const known = keys.map((key) => `_k === ${JSON.stringify(key)}`).join(" || ");
|
|
1027
|
+
parts.push(`Object.keys(${objAcc}).every((_k) => ${known})`);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
return parts;
|
|
1031
|
+
};
|
|
1032
|
+
const generateBooleanGuard = (schema, typeName, _suffix = "") => {
|
|
1033
|
+
const name = guardName(typeName);
|
|
1034
|
+
const fallback = `export const ${name} = (input: unknown): input is ${typeName} => ${validatorName(typeName)}(input) === true`;
|
|
1035
|
+
const rewritten = rewriteNullable(schema);
|
|
1036
|
+
if (isObjectSchema(rewritten)) {
|
|
1037
|
+
const parts = booleanObjectParts(rewritten, "input", "obj");
|
|
1038
|
+
if (parts === null)
|
|
1039
|
+
return fallback;
|
|
1040
|
+
return [
|
|
1041
|
+
`export const ${name} = (input: unknown): input is ${typeName} => {`,
|
|
1042
|
+
` const obj = input as Record<string, unknown>`,
|
|
1043
|
+
` return (`,
|
|
1044
|
+
parts.map((part) => ` ${part}`).join(" &&\n"),
|
|
1045
|
+
` )`,
|
|
1046
|
+
`}`
|
|
1047
|
+
].join("\n");
|
|
1048
|
+
}
|
|
1049
|
+
const expr = booleanLeafExpr(rewritten, "input");
|
|
1050
|
+
if (expr === null)
|
|
1051
|
+
return fallback;
|
|
1052
|
+
return `export const ${name} = (input: unknown): input is ${typeName} => ${expr}`;
|
|
1554
1053
|
};
|
|
1555
|
-
/**
|
|
1556
|
-
* Generates a validator function for a non-object schema (primitive, array, enum, $ref).
|
|
1557
|
-
*/
|
|
1558
1054
|
const generateScalarValidator = (schema, typeName, suffix) => {
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
}
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
return [
|
|
1665
|
-
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1666
|
-
` let errors: ValidationError[] | undefined`,
|
|
1667
|
-
body,
|
|
1668
|
-
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1669
|
-
`}`,
|
|
1670
|
-
].join('\n');
|
|
1671
|
-
}
|
|
1672
|
-
// Top-level multi-type / nullable schema (array `type`, e.g. `["string","null"]`).
|
|
1673
|
-
// `hasType` is false for an array `type`, so without this a root multi-type
|
|
1674
|
-
// schema falls through to the final `return true` and validates NOTHING. The
|
|
1675
|
-
// value is valid when it matches any listed type.
|
|
1676
|
-
const rootTypeArray = getTypeArray(schema);
|
|
1677
|
-
if (rootTypeArray) {
|
|
1678
|
-
const allWrong = rootTypeArray
|
|
1679
|
-
.map((t) => wrongTypeCondition('input', t))
|
|
1680
|
-
.filter((c) => c !== '')
|
|
1681
|
-
.map((c) => `(${c})`)
|
|
1682
|
-
.join(' && ');
|
|
1683
|
-
const label = rootTypeArray.map((t) => typeofString(t)).join(' or ');
|
|
1684
|
-
if (!allWrong) {
|
|
1685
|
-
return [
|
|
1686
|
-
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
1687
|
-
` return true`,
|
|
1688
|
-
`}`,
|
|
1689
|
-
].join('\n');
|
|
1690
|
-
}
|
|
1691
|
-
return [
|
|
1692
|
-
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1693
|
-
` if (${allWrong}) {`,
|
|
1694
|
-
` return { valid: false, errors: [{ message: ${JSON.stringify(`must be ${label}`)}, path: _path }] }`,
|
|
1695
|
-
` }`,
|
|
1696
|
-
` return true`,
|
|
1697
|
-
`}`,
|
|
1698
|
-
].join('\n');
|
|
1055
|
+
const vName = validatorName(typeName);
|
|
1056
|
+
if (!isSchemaObject(schema)) {
|
|
1057
|
+
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
|
|
1058
|
+
}
|
|
1059
|
+
if (hasRef(schema)) {
|
|
1060
|
+
const delegateName = validatorName(refToName(schema.$ref, suffix));
|
|
1061
|
+
return [
|
|
1062
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1063
|
+
` return ${delegateName}(input, _path)`,
|
|
1064
|
+
`}`
|
|
1065
|
+
].join("\n");
|
|
1066
|
+
}
|
|
1067
|
+
const instanceOf = getMjstInstanceOf(schema);
|
|
1068
|
+
if (instanceOf) {
|
|
1069
|
+
return [
|
|
1070
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1071
|
+
` if (!(input instanceof ${instanceOf})) {`,
|
|
1072
|
+
` return { valid: false, errors: [{ message: 'must be ${instanceOf}', path: _path }] }`,
|
|
1073
|
+
` }`,
|
|
1074
|
+
` return true`,
|
|
1075
|
+
`}`
|
|
1076
|
+
].join("\n");
|
|
1077
|
+
}
|
|
1078
|
+
const primitive = getMjstPrimitive(schema);
|
|
1079
|
+
if (primitive) {
|
|
1080
|
+
return [
|
|
1081
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1082
|
+
` if (typeof input !== "${primitive}") {`,
|
|
1083
|
+
` return { valid: false, errors: [{ message: 'must be ${primitive}', path: _path }] }`,
|
|
1084
|
+
` }`,
|
|
1085
|
+
` return true`,
|
|
1086
|
+
`}`
|
|
1087
|
+
].join("\n");
|
|
1088
|
+
}
|
|
1089
|
+
if (hasConst(schema)) {
|
|
1090
|
+
const mismatch = constMismatchCondition("input", schema.const);
|
|
1091
|
+
const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
|
|
1092
|
+
return [
|
|
1093
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1094
|
+
` if (${mismatch}) {`,
|
|
1095
|
+
` return { valid: false, errors: [{ message: ${msg}, path: _path }] }`,
|
|
1096
|
+
` }`,
|
|
1097
|
+
` return true`,
|
|
1098
|
+
`}`
|
|
1099
|
+
].join("\n");
|
|
1100
|
+
}
|
|
1101
|
+
if (hasEnum(schema)) {
|
|
1102
|
+
const allowed = JSON.stringify(schema.enum);
|
|
1103
|
+
const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
1104
|
+
return [
|
|
1105
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1106
|
+
` if (!(${allowed} as unknown[]).includes(input)) {`,
|
|
1107
|
+
` return { valid: false, errors: [{ message: ${JSON.stringify(`must be one of: ${label}`)}, path: _path }] }`,
|
|
1108
|
+
` }`,
|
|
1109
|
+
` return true`,
|
|
1110
|
+
`}`
|
|
1111
|
+
].join("\n");
|
|
1112
|
+
}
|
|
1113
|
+
if (hasAllOf(schema) || hasAnyOf(schema) || hasOneOf(schema) || "not" in schema || "if" in schema) {
|
|
1114
|
+
const ctx = createRootContext();
|
|
1115
|
+
const checks = [];
|
|
1116
|
+
const rootPath = "`${_path}`";
|
|
1117
|
+
const rootTypeArray2 = getTypeArray(schema);
|
|
1118
|
+
if (rootTypeArray2) {
|
|
1119
|
+
const allWrong = rootTypeArray2.map((t) => wrongTypeCondition("input", t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
1120
|
+
if (allWrong) {
|
|
1121
|
+
const label = rootTypeArray2.map((t) => typeofString(t)).join(" or ");
|
|
1122
|
+
checks.push(` if (${allWrong}) {`);
|
|
1123
|
+
checks.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${rootPath} })`);
|
|
1124
|
+
checks.push(` }`);
|
|
1125
|
+
}
|
|
1126
|
+
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1127
|
+
} else if (hasType(schema)) {
|
|
1128
|
+
const t = schema.type;
|
|
1129
|
+
const wrongType = wrongTypeCondition("input", t);
|
|
1130
|
+
if (wrongType) {
|
|
1131
|
+
checks.push(` if (${wrongType}) {`);
|
|
1132
|
+
checks.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${rootPath} })`);
|
|
1133
|
+
checks.push(` }`);
|
|
1134
|
+
}
|
|
1135
|
+
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1136
|
+
}
|
|
1137
|
+
checks.push(...generateCombinatorChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1138
|
+
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1139
|
+
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
|
|
1140
|
+
|
|
1141
|
+
` : "";
|
|
1142
|
+
return [
|
|
1143
|
+
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1144
|
+
` let errors: ValidationError[] | undefined`,
|
|
1145
|
+
body,
|
|
1146
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1147
|
+
`}`
|
|
1148
|
+
].join("\n");
|
|
1149
|
+
}
|
|
1150
|
+
const rootTypeArray = getTypeArray(schema);
|
|
1151
|
+
if (rootTypeArray) {
|
|
1152
|
+
const allWrong = rootTypeArray.map((t) => wrongTypeCondition("input", t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
1153
|
+
const label = rootTypeArray.map((t) => typeofString(t)).join(" or ");
|
|
1154
|
+
if (!allWrong) {
|
|
1155
|
+
return [
|
|
1156
|
+
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
1157
|
+
` return true`,
|
|
1158
|
+
`}`
|
|
1159
|
+
].join("\n");
|
|
1699
1160
|
}
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
// Array-item / nested constraints can hoist module-level declarations (e.g. a
|
|
1732
|
-
// compiled known-keys set); emit them before the function so it references them.
|
|
1733
|
-
const hoistedBlock = rootCtx.hoisted.length > 0 ? `${rootCtx.hoisted.join('\n')}\n\n` : '';
|
|
1734
|
-
return [
|
|
1735
|
-
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1736
|
-
` if (${wrongType}) {`,
|
|
1737
|
-
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1738
|
-
` }`,
|
|
1739
|
-
` let errors: ValidationError[] | undefined`,
|
|
1740
|
-
constraintLines.join('\n').replaceAll('errors.push(', '(errors ??= []).push('),
|
|
1741
|
-
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1742
|
-
`}`,
|
|
1743
|
-
].join('\n');
|
|
1161
|
+
return [
|
|
1162
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1163
|
+
` if (${allWrong}) {`,
|
|
1164
|
+
` return { valid: false, errors: [{ message: ${JSON.stringify(`must be ${label}`)}, path: _path }] }`,
|
|
1165
|
+
` }`,
|
|
1166
|
+
` return true`,
|
|
1167
|
+
`}`
|
|
1168
|
+
].join("\n");
|
|
1169
|
+
}
|
|
1170
|
+
if (hasType(schema)) {
|
|
1171
|
+
const t = schema.type;
|
|
1172
|
+
const wrongType = wrongTypeCondition("input", t);
|
|
1173
|
+
const typLabel = typeofString(t);
|
|
1174
|
+
const rootCtx = createRootContext();
|
|
1175
|
+
const constraintLines = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, rootCtx);
|
|
1176
|
+
if (!wrongType) {
|
|
1177
|
+
return [
|
|
1178
|
+
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
1179
|
+
` return true`,
|
|
1180
|
+
`}`
|
|
1181
|
+
].join("\n");
|
|
1182
|
+
}
|
|
1183
|
+
if (constraintLines.length === 0) {
|
|
1184
|
+
return [
|
|
1185
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1186
|
+
` if (${wrongType}) {`,
|
|
1187
|
+
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1188
|
+
` }`,
|
|
1189
|
+
` return true`,
|
|
1190
|
+
`}`
|
|
1191
|
+
].join("\n");
|
|
1744
1192
|
}
|
|
1745
|
-
|
|
1193
|
+
const hoistedBlock = rootCtx.hoisted.length > 0 ? `${rootCtx.hoisted.join("\n")}
|
|
1194
|
+
|
|
1195
|
+
` : "";
|
|
1196
|
+
return [
|
|
1197
|
+
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1198
|
+
` if (${wrongType}) {`,
|
|
1199
|
+
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1200
|
+
` }`,
|
|
1201
|
+
` let errors: ValidationError[] | undefined`,
|
|
1202
|
+
constraintLines.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
|
|
1203
|
+
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1204
|
+
`}`
|
|
1205
|
+
].join("\n");
|
|
1206
|
+
}
|
|
1207
|
+
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
|
|
1746
1208
|
};
|
|
1747
|
-
/**
|
|
1748
|
-
* Throws when a schema (anywhere in its subtree) uses a keyword this generator
|
|
1749
|
-
* does not implement but which *narrows* the set of valid documents. Today that
|
|
1750
|
-
* is `unevaluatedProperties` / `unevaluatedItems` with a constraining value
|
|
1751
|
-
* (`false` or a subschema). The generator has no support for them — only the
|
|
1752
|
-
* runtime interpreter does — so silently emitting a validator would produce one
|
|
1753
|
-
* that ACCEPTS documents the interpreter REJECTS: a wrong verdict, worse than an
|
|
1754
|
-
* error. `unevaluated*: true` is a no-op (it permits everything), so it is
|
|
1755
|
-
* allowed through.
|
|
1756
|
-
*
|
|
1757
|
-
* We deliberately throw rather than implement the keywords: doing them correctly
|
|
1758
|
-
* requires tracking which properties/items each combinator branch "evaluated",
|
|
1759
|
-
* which is a large, separate feature. Failing loudly at generation time surfaces
|
|
1760
|
-
* the gap instead of shipping a validator that lies.
|
|
1761
|
-
*/
|
|
1762
1209
|
const assertNoUnsupportedKeywords = (schema, typeName) => {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
visit(value);
|
|
1782
|
-
};
|
|
1783
|
-
visit(schema);
|
|
1210
|
+
const visit = (node) => {
|
|
1211
|
+
if (typeof node !== "object" || node === null)
|
|
1212
|
+
return;
|
|
1213
|
+
if (Array.isArray(node)) {
|
|
1214
|
+
for (const item of node)
|
|
1215
|
+
visit(item);
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
const record = node;
|
|
1219
|
+
for (const keyword of ["unevaluatedProperties", "unevaluatedItems"]) {
|
|
1220
|
+
if (keyword in record && record[keyword] !== true) {
|
|
1221
|
+
throw new Error(`[${typeName}] unsupported keyword "${keyword}": the validator generator does not implement it and would silently accept documents the interpreter rejects. Validate this schema with the runtime interpreter, or remove the keyword.`);
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
for (const value of Object.values(record))
|
|
1225
|
+
visit(value);
|
|
1226
|
+
};
|
|
1227
|
+
visit(schema);
|
|
1784
1228
|
};
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
'unevaluatedItems',
|
|
1229
|
+
const SINGLE_SUBSCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
1230
|
+
"additionalProperties",
|
|
1231
|
+
"additionalItems",
|
|
1232
|
+
"contains",
|
|
1233
|
+
"propertyNames",
|
|
1234
|
+
"not",
|
|
1235
|
+
"if",
|
|
1236
|
+
"then",
|
|
1237
|
+
"else",
|
|
1238
|
+
"unevaluatedProperties",
|
|
1239
|
+
"unevaluatedItems"
|
|
1797
1240
|
]);
|
|
1798
|
-
|
|
1799
|
-
const
|
|
1800
|
-
/** Keywords whose value is a map of names to subschemas. */
|
|
1801
|
-
const SUBSCHEMA_MAP_KEYS = new Set(['properties', 'patternProperties', 'dependentSchemas', '$defs', 'definitions']);
|
|
1802
|
-
/**
|
|
1803
|
-
* Rewrites OpenAPI 3.0 `nullable: true` into a form the generator already
|
|
1804
|
-
* enforces. A node `{ nullable: true, ...rest }` accepts a value iff the value is
|
|
1805
|
-
* `null` OR it matches `rest` — exactly `{ anyOf: [{ type: 'null' }, rest] }`.
|
|
1806
|
-
* Emitting that lets the existing `anyOf` machinery (and its guard bail-outs)
|
|
1807
|
-
* mirror the interpreter, which short-circuits every keyword when a `nullable`
|
|
1808
|
-
* node sees `null`.
|
|
1809
|
-
*
|
|
1810
|
-
* The walk descends only into genuine subschema positions (never into `enum` /
|
|
1811
|
-
* `const` / `default` data), so a data value that merely looks like a schema is
|
|
1812
|
-
* never rewritten. Returns a fresh tree; the caller's schema is untouched.
|
|
1813
|
-
*/
|
|
1241
|
+
const SUBSCHEMA_LIST_KEYS = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
|
|
1242
|
+
const SUBSCHEMA_MAP_KEYS = /* @__PURE__ */ new Set(["properties", "patternProperties", "dependentSchemas", "$defs", "definitions"]);
|
|
1814
1243
|
const rewriteNullable = (node) => {
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
}
|
|
1847
|
-
else {
|
|
1848
|
-
out[key] = value;
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
if (src['nullable'] === true)
|
|
1852
|
-
return { anyOf: [{ type: 'null' }, out] };
|
|
1853
|
-
return out;
|
|
1244
|
+
if (typeof node !== "object" || node === null || Array.isArray(node))
|
|
1245
|
+
return node;
|
|
1246
|
+
const src = node;
|
|
1247
|
+
const out = {};
|
|
1248
|
+
for (const [key, value] of Object.entries(src)) {
|
|
1249
|
+
if (key === "nullable")
|
|
1250
|
+
continue;
|
|
1251
|
+
if (SINGLE_SUBSCHEMA_KEYS.has(key)) {
|
|
1252
|
+
out[key] = rewriteNullable(value);
|
|
1253
|
+
} else if (key === "items") {
|
|
1254
|
+
out[key] = Array.isArray(value) ? value.map(rewriteNullable) : rewriteNullable(value);
|
|
1255
|
+
} else if (SUBSCHEMA_LIST_KEYS.has(key) && Array.isArray(value)) {
|
|
1256
|
+
out[key] = value.map(rewriteNullable);
|
|
1257
|
+
} else if (SUBSCHEMA_MAP_KEYS.has(key) && typeof value === "object" && value !== null) {
|
|
1258
|
+
const mapped = {};
|
|
1259
|
+
for (const [name, sub] of Object.entries(value))
|
|
1260
|
+
mapped[name] = rewriteNullable(sub);
|
|
1261
|
+
out[key] = mapped;
|
|
1262
|
+
} else if (key === "dependencies" && typeof value === "object" && value !== null) {
|
|
1263
|
+
const mapped = {};
|
|
1264
|
+
for (const [name, sub] of Object.entries(value)) {
|
|
1265
|
+
mapped[name] = Array.isArray(sub) ? sub : rewriteNullable(sub);
|
|
1266
|
+
}
|
|
1267
|
+
out[key] = mapped;
|
|
1268
|
+
} else {
|
|
1269
|
+
out[key] = value;
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
if (src["nullable"] === true)
|
|
1273
|
+
return { anyOf: [{ type: "null" }, out] };
|
|
1274
|
+
return out;
|
|
1854
1275
|
};
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
* ```typescript
|
|
1867
|
-
* generateValidatorFunction({ type: 'object', properties: { name: { type: 'string' } }, required: ['name'] }, 'Info')
|
|
1868
|
-
* // export const validateInfo = (input: unknown, _path = ''): ValidationResult => {
|
|
1869
|
-
* // if (typeof input !== 'object' || ...) return { valid: false, ... }
|
|
1870
|
-
* // const errors: ValidationError[] = []
|
|
1871
|
-
* // const obj = input as Record<string, unknown>
|
|
1872
|
-
* // if (!('name' in obj)) { errors.push(...) } else if (typeof obj['name'] !== 'string') { errors.push(...) }
|
|
1873
|
-
* // return errors.length > 0 ? { valid: false, errors } : true
|
|
1874
|
-
* // }
|
|
1875
|
-
* ```
|
|
1876
|
-
*/
|
|
1877
|
-
export const generateValidatorFunction = (schema, typeName, suffix = '') => {
|
|
1878
|
-
assertNoUnsupportedKeywords(schema, typeName);
|
|
1879
|
-
// Fold OpenAPI `nullable: true` into the `anyOf` form the generator already
|
|
1880
|
-
// enforces, so the emitted checks match the interpreter's null short-circuit.
|
|
1881
|
-
const rewritten = rewriteNullable(schema);
|
|
1882
|
-
if (isObjectSchema(rewritten)) {
|
|
1883
|
-
return generateObjectValidator(rewritten, typeName, suffix);
|
|
1884
|
-
}
|
|
1885
|
-
return generateScalarValidator(rewritten, typeName, suffix);
|
|
1276
|
+
const generateValidatorFunction = (schema, typeName, suffix = "") => {
|
|
1277
|
+
assertNoUnsupportedKeywords(schema, typeName);
|
|
1278
|
+
const rewritten = rewriteNullable(schema);
|
|
1279
|
+
if (isObjectSchema(rewritten)) {
|
|
1280
|
+
return generateObjectValidator(rewritten, typeName, suffix);
|
|
1281
|
+
}
|
|
1282
|
+
return generateScalarValidator(rewritten, typeName, suffix);
|
|
1283
|
+
};
|
|
1284
|
+
export {
|
|
1285
|
+
generateBooleanGuard,
|
|
1286
|
+
generateValidatorFunction
|
|
1886
1287
|
};
|