@amritk/generate-validators 0.12.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AI.md +3 -2
- package/README.md +8 -6
- package/dist/generators/assert-unevaluated-generatable.js +3 -5
- package/dist/generators/build-schema.d.ts +6 -0
- package/dist/generators/build-schema.js +22 -2
- package/dist/generators/collect-emitted-refs.d.ts +1 -1
- package/dist/generators/collect-emitted-refs.js +45 -13
- package/dist/generators/collect-validator-imports.js +3 -8
- package/dist/generators/enforced-keywords.d.ts +21 -0
- package/dist/generators/enforced-keywords.js +60 -0
- package/dist/generators/folds-to-constant.d.ts +15 -0
- package/dist/generators/folds-to-constant.js +39 -0
- package/dist/generators/generate-files.js +4 -3
- package/dist/generators/generate-validator-function.js +288 -330
- package/dist/generators/tuple-shape.d.ts +31 -0
- package/dist/generators/tuple-shape.js +18 -0
- package/dist/generators/unevaluated-match.js +12 -3
- package/package.json +2 -2
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { regexFlagsFor, regexLiteral } from "@amritk/helpers/escape-regex-pattern";
|
|
2
|
-
import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
|
|
2
|
+
import { getMjstInstanceOf, getMjstPrimitive, MJST_EXTENSION_KEY } from "@amritk/helpers/mjst-extension";
|
|
3
3
|
import { multipleOfFailExpr, multipleOfPassExpr } from "@amritk/helpers/multiple-of-check";
|
|
4
4
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
5
5
|
import { safeAccessor } from "@amritk/helpers/safe-accessor";
|
|
@@ -8,6 +8,8 @@ import { maxLengthFailExpr, maxLengthPassExpr, minLengthFailExpr, minLengthPassE
|
|
|
8
8
|
import { unknownKeyCheck } from "@amritk/helpers/unknown-key-check";
|
|
9
9
|
import { assertGeneratableRefs } from "./assert-generatable-refs.js";
|
|
10
10
|
import { assertUnevaluatedGeneratable, UNPROVABLE_COVERAGE_MESSAGE } from "./assert-unevaluated-generatable.js";
|
|
11
|
+
import { declaresKeywordOutside } from "./enforced-keywords.js";
|
|
12
|
+
import { tupleShapeOf } from "./tuple-shape.js";
|
|
11
13
|
import { unevaluatedItemsExpr, unevaluatedPropertiesExpr } from "./unevaluated-match.js";
|
|
12
14
|
const validatorName = (typeName) => `validate${typeName}`;
|
|
13
15
|
const propertyRead = (objVar, key) => safeAccessor(objVar, key);
|
|
@@ -54,16 +56,15 @@ const schemaIsScalarOnly = (schema) => {
|
|
|
54
56
|
return types.length > 0 && types.every((x) => typeof x === "string" && SCALAR_ITEM_TYPES.has(x));
|
|
55
57
|
};
|
|
56
58
|
const arrayItemsAreScalarOnly = (schema) => {
|
|
57
|
-
const
|
|
58
|
-
if (
|
|
59
|
-
if (!
|
|
59
|
+
const { tuple, tail, tailIsClosed } = tupleShapeOf(schema);
|
|
60
|
+
if (tuple !== void 0) {
|
|
61
|
+
if (!tuple.every((p) => schemaIsScalarOnly(p)))
|
|
60
62
|
return false;
|
|
61
|
-
|
|
62
|
-
if (tail === false)
|
|
63
|
+
if (tailIsClosed)
|
|
63
64
|
return true;
|
|
64
65
|
return schemaIsScalarOnly(tail);
|
|
65
66
|
}
|
|
66
|
-
return schemaIsScalarOnly(
|
|
67
|
+
return schemaIsScalarOnly(tail);
|
|
67
68
|
};
|
|
68
69
|
const wrongTypeCondition = (accessor, type) => {
|
|
69
70
|
switch (type) {
|
|
@@ -86,6 +87,7 @@ const wrongTypeCondition = (accessor, type) => {
|
|
|
86
87
|
}
|
|
87
88
|
};
|
|
88
89
|
const declaresObjectType = (schema) => hasType(schema) && schema.type === "object";
|
|
90
|
+
const objectRootIsSelfContained = (schema) => !hasRef(schema) && !hasConst(schema) && !hasEnum(schema) && getMjstInstanceOf(schema) === void 0 && getMjstPrimitive(schema) === void 0;
|
|
89
91
|
const getTypeArray = (schema) => {
|
|
90
92
|
if (!isSchemaObject(schema) || !("type" in schema) || !Array.isArray(schema.type))
|
|
91
93
|
return null;
|
|
@@ -107,6 +109,15 @@ const patternPropertySources = (schema) => {
|
|
|
107
109
|
return [];
|
|
108
110
|
return Object.keys(patterns);
|
|
109
111
|
};
|
|
112
|
+
const readsObjBinding = (text) => /\bobj\b/.test(text);
|
|
113
|
+
const withHoisted = (hoisted, text) => {
|
|
114
|
+
const kept = hoisted.filter((entry) => text.includes(entry.reference)).map((entry) => entry.declaration);
|
|
115
|
+
const reads = text.replaceAll("(input: unknown", "(");
|
|
116
|
+
const body = /\binput\b/.test(reads) ? text : text.replaceAll("(input: unknown", "(_input: unknown");
|
|
117
|
+
return kept.length > 0 ? `${kept.join("\n")}
|
|
118
|
+
|
|
119
|
+
${body}` : body;
|
|
120
|
+
};
|
|
110
121
|
const generateStrictKeyChecks = (schema, ctx) => {
|
|
111
122
|
if (!isSchemaObject(schema))
|
|
112
123
|
return [];
|
|
@@ -114,20 +125,27 @@ const generateStrictKeyChecks = (schema, ctx) => {
|
|
|
114
125
|
return [];
|
|
115
126
|
const known = Object.keys(hasProperties(schema) ? schema.properties : {});
|
|
116
127
|
const d = ctx.depth;
|
|
117
|
-
const
|
|
118
|
-
|
|
128
|
+
const knownKeysName = `_knownKeys${ctx.hoisted.length}`;
|
|
129
|
+
const check = unknownKeyCheck(known, knownKeysName);
|
|
130
|
+
const unknownTest = check.isUnknown(`_key${d}`);
|
|
131
|
+
for (const declaration of check.declarations) {
|
|
132
|
+
ctx.hoisted.push({ declaration, reference: unknownTest });
|
|
133
|
+
}
|
|
119
134
|
const patterns = patternPropertySources(schema);
|
|
120
135
|
let patternGuard = "";
|
|
121
136
|
if (patterns.length > 0) {
|
|
122
137
|
const patternsName = `_patterns${ctx.hoisted.length}`;
|
|
123
138
|
const compiled = patterns.map((p) => `new RegExp(${JSON.stringify(p)}, ${JSON.stringify(regexFlagsFor(p))})`);
|
|
124
|
-
ctx.hoisted.push(`const ${patternsName} = [${compiled.join(", ")}]`);
|
|
125
139
|
patternGuard = ` && !${patternsName}.some((re) => re.test(_key${d}))`;
|
|
140
|
+
ctx.hoisted.push({
|
|
141
|
+
declaration: `const ${patternsName} = [${compiled.join(", ")}]`,
|
|
142
|
+
reference: patternGuard
|
|
143
|
+
});
|
|
126
144
|
}
|
|
127
145
|
return [
|
|
128
146
|
` for (const _key${d} in ${ctx.objVar}) {`,
|
|
129
|
-
` if (${
|
|
130
|
-
` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${_key${d}}\` })`,
|
|
147
|
+
` if (${unknownTest}${patternGuard}) {`,
|
|
148
|
+
` errors.push({ message: 'must NOT have additional properties', path: \`${ctx.pathPrefix}/\${escapePointer(_key${d})}\` })`,
|
|
131
149
|
` }`,
|
|
132
150
|
` }`
|
|
133
151
|
];
|
|
@@ -179,172 +197,109 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
|
|
|
179
197
|
return lines;
|
|
180
198
|
return [` const ${protoLocalName(key, ctx.depth)} = ${propertyRead(ctx.objVar, key)}`, ...lines];
|
|
181
199
|
};
|
|
182
|
-
const
|
|
183
|
-
if (
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
}
|
|
192
|
-
return [];
|
|
200
|
+
const singleIfBlock = (lines) => {
|
|
201
|
+
if (lines.length < 3)
|
|
202
|
+
return null;
|
|
203
|
+
const opener = /^ {2}if \((.*)\) \{$/.exec(lines[0]);
|
|
204
|
+
if (opener === null || lines[lines.length - 1] !== " }")
|
|
205
|
+
return null;
|
|
206
|
+
for (let i = 1; i < lines.length - 1; i++) {
|
|
207
|
+
if (!lines[i].startsWith(" "))
|
|
208
|
+
return null;
|
|
193
209
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
210
|
+
return { condition: opener[1], body: lines.slice(1, -1) };
|
|
211
|
+
};
|
|
212
|
+
const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) => {
|
|
213
|
+
if (!isSchemaObject(schema))
|
|
214
|
+
return [];
|
|
197
215
|
const lines = [];
|
|
198
|
-
if (hasRef(
|
|
199
|
-
const
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx),
|
|
203
|
-
...generateCombinatorChecks(key, raw, path, propSchema, suffix, ctx)
|
|
204
|
-
];
|
|
205
|
-
const delegate = [
|
|
206
|
-
` const _r = ${vName}(${raw}, ${path})`,
|
|
207
|
-
` if (_r !== true) errors.push(..._r.errors)`,
|
|
208
|
-
...siblings
|
|
209
|
-
];
|
|
210
|
-
if (isRequired) {
|
|
211
|
-
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
212
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
213
|
-
lines.push(` } else {`);
|
|
214
|
-
lines.push(...delegate);
|
|
215
|
-
lines.push(` }`);
|
|
216
|
-
} else {
|
|
217
|
-
lines.push(` if (${raw} !== undefined) {`);
|
|
216
|
+
if (hasRef(schema)) {
|
|
217
|
+
const vName = validatorName(refToName(schema.$ref, suffix));
|
|
218
|
+
const delegate = [` const _r = ${vName}(${raw}, ${path})`, ` if (_r !== true) errors.push(..._r.errors)`];
|
|
219
|
+
if (presence === "")
|
|
218
220
|
lines.push(...delegate);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
return lines;
|
|
221
|
+
else
|
|
222
|
+
lines.push(` if (${raw} !== undefined) {`, ...delegate.map((line) => ` ${line}`), ` }`);
|
|
222
223
|
}
|
|
223
|
-
const instanceOf = getMjstInstanceOf(
|
|
224
|
+
const instanceOf = getMjstInstanceOf(schema);
|
|
224
225
|
if (instanceOf) {
|
|
225
|
-
if (
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
|
|
229
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
230
|
-
lines.push(` }`);
|
|
231
|
-
} else {
|
|
232
|
-
lines.push(` if (${raw} !== undefined && !(${raw} instanceof ${instanceOf})) {`);
|
|
233
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
234
|
-
lines.push(` }`);
|
|
235
|
-
}
|
|
236
|
-
return lines;
|
|
226
|
+
lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
|
|
227
|
+
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
228
|
+
lines.push(` }`);
|
|
237
229
|
}
|
|
238
|
-
const primitive = getMjstPrimitive(
|
|
230
|
+
const primitive = getMjstPrimitive(schema);
|
|
239
231
|
if (primitive) {
|
|
240
|
-
if (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
|
|
244
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
245
|
-
lines.push(` }`);
|
|
246
|
-
} else {
|
|
247
|
-
lines.push(` if (${raw} !== undefined && typeof ${raw} !== "${primitive}") {`);
|
|
248
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
249
|
-
lines.push(` }`);
|
|
250
|
-
}
|
|
251
|
-
return lines;
|
|
232
|
+
lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
|
|
233
|
+
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
234
|
+
lines.push(` }`);
|
|
252
235
|
}
|
|
253
|
-
if (hasConst(
|
|
254
|
-
const mismatch = constMismatchCondition(raw,
|
|
255
|
-
const msg = JSON.stringify(`must be ${JSON.stringify(
|
|
256
|
-
if (
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
lines.push(` } else if (${mismatch}) {`);
|
|
260
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
261
|
-
lines.push(` }`);
|
|
262
|
-
} else {
|
|
263
|
-
lines.push(` if (${raw} !== undefined && ${mismatch}) {`);
|
|
264
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
265
|
-
lines.push(` }`);
|
|
266
|
-
}
|
|
267
|
-
return lines;
|
|
236
|
+
if (hasConst(schema)) {
|
|
237
|
+
const mismatch = constMismatchCondition(raw, schema.const);
|
|
238
|
+
const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
|
|
239
|
+
lines.push(` if (${presence}${mismatch}) {`);
|
|
240
|
+
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
241
|
+
lines.push(` }`);
|
|
268
242
|
}
|
|
269
|
-
if (hasEnum(
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
275
|
-
lines.push(` } else if (!${member}) {`);
|
|
276
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
277
|
-
lines.push(` }`);
|
|
278
|
-
} else {
|
|
279
|
-
lines.push(` if (${raw} !== undefined && !${member}) {`);
|
|
280
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
281
|
-
lines.push(` }`);
|
|
282
|
-
}
|
|
283
|
-
return lines;
|
|
243
|
+
if (hasEnum(schema)) {
|
|
244
|
+
const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
245
|
+
lines.push(` if (${presence}!${enumMembershipExpr(schema.enum, raw)}) {`);
|
|
246
|
+
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
247
|
+
lines.push(` }`);
|
|
284
248
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
249
|
+
if (instanceOf === void 0 && primitive === void 0) {
|
|
250
|
+
if (hasType(schema)) {
|
|
251
|
+
const t = schema.type;
|
|
252
|
+
const wrongType = wrongTypeCondition(raw, t);
|
|
253
|
+
if (wrongType) {
|
|
254
|
+
lines.push(` if (${presence === "" ? wrongType : `${presence}(${wrongType})`}) {`);
|
|
255
|
+
lines.push(` errors.push({ message: 'must be ${typeofString(t)}', path: ${path} })`);
|
|
256
|
+
lines.push(` }`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
const typeArray = getTypeArray(schema);
|
|
260
|
+
if (typeArray) {
|
|
261
|
+
const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
292
262
|
if (allWrong) {
|
|
293
|
-
|
|
263
|
+
const label = typeArray.map((t) => typeofString(t)).join(" or ");
|
|
264
|
+
lines.push(` if (${presence === "" ? allWrong : `${presence}(${allWrong})`}) {`);
|
|
294
265
|
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
266
|
+
lines.push(` }`);
|
|
295
267
|
}
|
|
296
|
-
lines.push(` }`);
|
|
297
|
-
} else if (allWrong) {
|
|
298
|
-
lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
|
|
299
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
300
|
-
lines.push(` }`);
|
|
301
268
|
}
|
|
302
|
-
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
303
|
-
return lines;
|
|
304
269
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
lines.push(`
|
|
312
|
-
if (wrongType) {
|
|
313
|
-
lines.push(` } else if (${wrongType}) {`);
|
|
314
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
315
|
-
}
|
|
316
|
-
lines.push(` }`);
|
|
317
|
-
} else if (wrongType) {
|
|
318
|
-
lines.push(` if (${raw} !== undefined && (${wrongType})) {`);
|
|
319
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
320
|
-
lines.push(` }`);
|
|
321
|
-
}
|
|
322
|
-
lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
|
|
270
|
+
lines.push(...generateConstraintChecks(key, raw, path, schema, suffix, ctx));
|
|
271
|
+
const combinators = generateCombinatorChecks(key, raw, path, schema, suffix, ctx);
|
|
272
|
+
if (combinators.length > 0) {
|
|
273
|
+
if (presence === "")
|
|
274
|
+
lines.push(...combinators);
|
|
275
|
+
else
|
|
276
|
+
lines.push(` if (${raw} !== undefined) {`, ...combinators.map((line) => ` ${line}`), ` }`);
|
|
323
277
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
278
|
+
return lines;
|
|
279
|
+
};
|
|
280
|
+
const generatePropertyCheckLines = (key, propSchema, isRequired, suffix, ctx) => {
|
|
281
|
+
const parentPath = ctx.depth === 0 ? "_path" : `\`${ctx.pathPrefix}\``;
|
|
282
|
+
const missing = [
|
|
283
|
+
` if (${missingCheck(ctx.objVar, key)}) {`,
|
|
284
|
+
` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`
|
|
327
285
|
];
|
|
328
|
-
if (
|
|
329
|
-
if (
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
lines.push(` }`);
|
|
333
|
-
}
|
|
334
|
-
} else if (isRequired) {
|
|
335
|
-
lines.push(` if (${missingCheck(ctx.objVar, key)}) {`);
|
|
336
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
|
|
337
|
-
if (extraLines.length > 0) {
|
|
338
|
-
lines.push(` } else {`);
|
|
339
|
-
lines.push(...extraLines);
|
|
340
|
-
}
|
|
341
|
-
lines.push(` }`);
|
|
342
|
-
} else if (extraLines.length > 0) {
|
|
343
|
-
lines.push(` if (${raw} !== undefined) {`);
|
|
344
|
-
lines.push(...extraLines);
|
|
345
|
-
lines.push(` }`);
|
|
286
|
+
if (!isSchemaObject(propSchema)) {
|
|
287
|
+
if (isRequired && propSchema === true)
|
|
288
|
+
return [...missing, ` }`];
|
|
289
|
+
return [];
|
|
346
290
|
}
|
|
347
|
-
|
|
291
|
+
const raw = PROTOTYPE_MEMBERS.has(key) ? protoLocalName(key, ctx.depth) : propertyRead(ctx.objVar, key);
|
|
292
|
+
const path = `\`${ctx.pathPrefix}/${pointerSegment(key)}\``;
|
|
293
|
+
if (isRequired) {
|
|
294
|
+
const valueLines = generateKeywordChecks(key, raw, path, propSchema, suffix, ctx, "");
|
|
295
|
+
if (valueLines.length === 0)
|
|
296
|
+
return [...missing, ` }`];
|
|
297
|
+
const single = singleIfBlock(valueLines);
|
|
298
|
+
if (single !== null)
|
|
299
|
+
return [...missing, ` } else if (${single.condition}) {`, ...single.body, ` }`];
|
|
300
|
+
return [...missing, ` } else {`, ...valueLines.map((line) => ` ${line}`), ` }`];
|
|
301
|
+
}
|
|
302
|
+
return generateKeywordChecks(key, raw, path, propSchema, suffix, ctx, `${raw} !== undefined && `);
|
|
348
303
|
};
|
|
349
304
|
const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
350
305
|
if (!isSchemaObject(propSchema))
|
|
@@ -359,7 +314,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
359
314
|
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
360
315
|
lines.push(` }`);
|
|
361
316
|
}
|
|
362
|
-
if (hasMinLength(propSchema)) {
|
|
317
|
+
if (hasMinLength(propSchema) && propSchema.minLength > 0) {
|
|
363
318
|
lines.push(` if (typeof ${raw} === 'string' && ${minLengthFailExpr(raw, propSchema.minLength)}) {`);
|
|
364
319
|
lines.push(` errors.push({ message: 'must have at least ${propSchema.minLength} characters', path: ${path} })`);
|
|
365
320
|
lines.push(` }`);
|
|
@@ -401,13 +356,13 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
401
356
|
lines.push(` }`);
|
|
402
357
|
}
|
|
403
358
|
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
const
|
|
407
|
-
const firstTailIndex =
|
|
359
|
+
const { tuple, tail, tailIsClosed } = tupleShapeOf(sp);
|
|
360
|
+
if (tail !== void 0) {
|
|
361
|
+
const itemSchema = tail;
|
|
362
|
+
const firstTailIndex = tuple !== void 0 ? tuple.length : 0;
|
|
408
363
|
const iv = `_i${ctx.depth}`;
|
|
409
364
|
const itemPath = `\`${path.slice(1, -1)}/\${${iv}}\``;
|
|
410
|
-
if (hasRef(itemSchema)) {
|
|
365
|
+
if (hasRef(itemSchema) && !declaresKeywordOutside(itemSchema, ["$ref"])) {
|
|
411
366
|
const vName = validatorName(refToName(itemSchema.$ref, suffix));
|
|
412
367
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
413
368
|
lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
|
|
@@ -428,8 +383,8 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
428
383
|
}
|
|
429
384
|
}
|
|
430
385
|
}
|
|
431
|
-
if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || "contains" in sp ||
|
|
432
|
-
if (
|
|
386
|
+
if (hasMinItems(propSchema) || hasMaxItems(propSchema) || hasUniqueItems(propSchema) && propSchema.uniqueItems === true || "contains" in sp || tuple !== void 0 || tail === false && tuple === void 0) {
|
|
387
|
+
if (tail === false && tuple === void 0) {
|
|
433
388
|
lines.push(` if (Array.isArray(${raw}) && ${raw}.length > 0) {`);
|
|
434
389
|
lines.push(` errors.push({ message: 'must NOT have more than 0 items', path: ${path} })`);
|
|
435
390
|
lines.push(` }`);
|
|
@@ -462,20 +417,19 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
|
|
|
462
417
|
lines.push(` }`);
|
|
463
418
|
lines.push(` }`);
|
|
464
419
|
}
|
|
465
|
-
|
|
466
|
-
if (Array.isArray(prefix)) {
|
|
420
|
+
if (tuple !== void 0) {
|
|
467
421
|
lines.push(` if (Array.isArray(${raw})) {`);
|
|
468
|
-
for (let i = 0; i <
|
|
469
|
-
const itemChecks = generateValueChecks("", `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``,
|
|
422
|
+
for (let i = 0; i < tuple.length; i++) {
|
|
423
|
+
const itemChecks = generateValueChecks("", `${raw}[${i}]`, `\`${path.slice(1, -1)}/${i}\``, tuple[i], suffix, ctx, true);
|
|
470
424
|
if (itemChecks.length > 0) {
|
|
471
425
|
lines.push(` if (${raw}.length > ${i}) {`);
|
|
472
426
|
lines.push(...itemChecks.map((l) => ` ${l}`));
|
|
473
427
|
lines.push(` }`);
|
|
474
428
|
}
|
|
475
429
|
}
|
|
476
|
-
if (
|
|
477
|
-
lines.push(` if (${raw}.length > ${
|
|
478
|
-
lines.push(` errors.push({ message: 'must NOT have more than ${
|
|
430
|
+
if (tailIsClosed) {
|
|
431
|
+
lines.push(` if (${raw}.length > ${tuple.length}) {`);
|
|
432
|
+
lines.push(` errors.push({ message: 'must NOT have more than ${tuple.length} items', path: ${path} })`);
|
|
479
433
|
lines.push(` }`);
|
|
480
434
|
}
|
|
481
435
|
lines.push(` }`);
|
|
@@ -503,68 +457,6 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
|
|
|
503
457
|
if (!isSchemaObject(propSchema))
|
|
504
458
|
return [];
|
|
505
459
|
const presence = required ? "" : `${raw} !== undefined && `;
|
|
506
|
-
if (hasRef(propSchema)) {
|
|
507
|
-
const vName = validatorName(refToName(propSchema.$ref, suffix));
|
|
508
|
-
if (required) {
|
|
509
|
-
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
510
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
511
|
-
} else {
|
|
512
|
-
lines.push(` if (${raw} !== undefined) {`);
|
|
513
|
-
lines.push(` const _r = ${vName}(${raw}, ${path})`);
|
|
514
|
-
lines.push(` if (_r !== true) errors.push(..._r.errors)`);
|
|
515
|
-
lines.push(` }`);
|
|
516
|
-
}
|
|
517
|
-
return lines;
|
|
518
|
-
}
|
|
519
|
-
const instanceOf = getMjstInstanceOf(propSchema);
|
|
520
|
-
if (instanceOf) {
|
|
521
|
-
lines.push(` if (${presence}!(${raw} instanceof ${instanceOf})) {`);
|
|
522
|
-
lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
|
|
523
|
-
lines.push(` }`);
|
|
524
|
-
return lines;
|
|
525
|
-
}
|
|
526
|
-
const primitive = getMjstPrimitive(propSchema);
|
|
527
|
-
if (primitive) {
|
|
528
|
-
lines.push(` if (${presence}typeof ${raw} !== "${primitive}") {`);
|
|
529
|
-
lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
|
|
530
|
-
lines.push(` }`);
|
|
531
|
-
return lines;
|
|
532
|
-
}
|
|
533
|
-
if (hasConst(propSchema)) {
|
|
534
|
-
const mismatch = constMismatchCondition(raw, propSchema.const);
|
|
535
|
-
const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
|
|
536
|
-
lines.push(` if (${presence}${mismatch}) {`);
|
|
537
|
-
lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
|
|
538
|
-
lines.push(` }`);
|
|
539
|
-
return lines;
|
|
540
|
-
}
|
|
541
|
-
if (hasEnum(propSchema)) {
|
|
542
|
-
const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
543
|
-
lines.push(` if (${presence}!${enumMembershipExpr(propSchema.enum, raw)}) {`);
|
|
544
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
|
|
545
|
-
lines.push(` }`);
|
|
546
|
-
return lines;
|
|
547
|
-
}
|
|
548
|
-
if (hasType(propSchema)) {
|
|
549
|
-
const t = propSchema.type;
|
|
550
|
-
const wrongType = wrongTypeCondition(raw, t);
|
|
551
|
-
const typLabel = typeofString(t);
|
|
552
|
-
if (wrongType) {
|
|
553
|
-
lines.push(` if (${presence}(${wrongType})) {`);
|
|
554
|
-
lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
|
|
555
|
-
lines.push(` }`);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
const typeArray = getTypeArray(propSchema);
|
|
559
|
-
if (typeArray) {
|
|
560
|
-
const allWrong = typeArray.map((t) => wrongTypeCondition(raw, t)).filter((c) => c !== "").map((c) => `(${c})`).join(" && ");
|
|
561
|
-
if (allWrong) {
|
|
562
|
-
const label = typeArray.map((t) => typeofString(t)).join(" or ");
|
|
563
|
-
lines.push(` if (${presence}(${allWrong})) {`);
|
|
564
|
-
lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
|
|
565
|
-
lines.push(` }`);
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
460
|
const valueCtx = {
|
|
569
461
|
objVar: ctx.objVar,
|
|
570
462
|
pathPrefix: path.slice(1, -1),
|
|
@@ -572,10 +464,10 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
|
|
|
572
464
|
hoisted: ctx.hoisted,
|
|
573
465
|
rootSchema: ctx.rootSchema
|
|
574
466
|
};
|
|
575
|
-
lines.push(...
|
|
576
|
-
lines.push(...generateCombinatorChecks("", raw, path, propSchema, suffix, valueCtx));
|
|
467
|
+
lines.push(...generateKeywordChecks("", raw, path, propSchema, suffix, valueCtx, presence));
|
|
577
468
|
return lines;
|
|
578
469
|
};
|
|
470
|
+
const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
|
|
579
471
|
const generateMatchesExpr = (raw, sub, suffix, ctx) => {
|
|
580
472
|
if (sub === true)
|
|
581
473
|
return "true";
|
|
@@ -583,12 +475,15 @@ const generateMatchesExpr = (raw, sub, suffix, ctx) => {
|
|
|
583
475
|
return "false";
|
|
584
476
|
if (!isSchemaObject(sub))
|
|
585
477
|
return "true";
|
|
586
|
-
const
|
|
478
|
+
const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
|
|
479
|
+
const checks = generateValueChecks("", value, "`${_path}`", sub, suffix, ctx);
|
|
587
480
|
if (checks.length === 0)
|
|
588
481
|
return "true";
|
|
589
482
|
const body = checks.join("\n").replaceAll("errors.push(", "_m.push(");
|
|
483
|
+
const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
|
|
484
|
+
`;
|
|
590
485
|
return `((): boolean => { const _m: ValidationError[] = []
|
|
591
|
-
${body}
|
|
486
|
+
${binding}${body}
|
|
592
487
|
return _m.length === 0 })()`;
|
|
593
488
|
};
|
|
594
489
|
const unevaluatedMatcher = (suffix, ctx) => (accessor, schema, depth) => generateMatchesExpr(accessor, schema, suffix, { ...ctx, depth: ctx.depth + depth + 1 });
|
|
@@ -636,9 +531,11 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
|
636
531
|
}
|
|
637
532
|
if (hasAnyOf(schema) && schema.anyOf.length > 0) {
|
|
638
533
|
const conds = schema.anyOf.map((b) => generateMatchesExpr(raw, b, suffix, ctx));
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
534
|
+
if (!conds.includes("true")) {
|
|
535
|
+
lines.push(` if (!(${conds.join(" || ")})) {`);
|
|
536
|
+
lines.push(` errors.push({ message: 'must match a schema in anyOf', path: ${path} })`);
|
|
537
|
+
lines.push(` }`);
|
|
538
|
+
}
|
|
642
539
|
}
|
|
643
540
|
if (hasOneOf(schema) && schema.oneOf.length > 0) {
|
|
644
541
|
const conds = schema.oneOf.map((b) => `(${generateMatchesExpr(raw, b, suffix, ctx)} ? 1 : 0)`);
|
|
@@ -649,9 +546,11 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
|
649
546
|
const not = schema["not"];
|
|
650
547
|
if (not !== void 0 && (isSchemaObject(not) || typeof not === "boolean")) {
|
|
651
548
|
const cond = generateMatchesExpr(raw, not, suffix, ctx);
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
549
|
+
if (cond !== "false") {
|
|
550
|
+
lines.push(` if (${cond}) {`);
|
|
551
|
+
lines.push(` errors.push({ message: 'must NOT match the schema in not', path: ${path} })`);
|
|
552
|
+
lines.push(` }`);
|
|
553
|
+
}
|
|
655
554
|
}
|
|
656
555
|
const ifSchema = schema["if"];
|
|
657
556
|
if (ifSchema !== void 0 && (isSchemaObject(ifSchema) || typeof ifSchema === "boolean")) {
|
|
@@ -660,11 +559,18 @@ const generateCombinatorChecks = (key, raw, path, schema, suffix, ctx) => {
|
|
|
660
559
|
const thenLines = thenSchema !== void 0 ? generateValueChecks(key, raw, path, thenSchema, suffix, ctx) : [];
|
|
661
560
|
const elseLines = elseSchema !== void 0 ? generateValueChecks(key, raw, path, elseSchema, suffix, ctx) : [];
|
|
662
561
|
if (thenLines.length > 0 || elseLines.length > 0) {
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
562
|
+
const cond = generateMatchesExpr(raw, ifSchema, suffix, ctx);
|
|
563
|
+
if (cond === "true") {
|
|
564
|
+
lines.push(...thenLines);
|
|
565
|
+
} else if (cond === "false") {
|
|
566
|
+
lines.push(...elseLines);
|
|
567
|
+
} else {
|
|
568
|
+
lines.push(` if (${cond}) {`);
|
|
569
|
+
lines.push(...thenLines);
|
|
570
|
+
lines.push(` } else {`);
|
|
571
|
+
lines.push(...elseLines);
|
|
572
|
+
lines.push(` }`);
|
|
573
|
+
}
|
|
668
574
|
}
|
|
669
575
|
}
|
|
670
576
|
return lines;
|
|
@@ -680,7 +586,7 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
|
|
|
680
586
|
for (const [pattern, sub] of patternEntries) {
|
|
681
587
|
const re = regexLiteral(pattern);
|
|
682
588
|
const kv = `_pk${d}`;
|
|
683
|
-
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, sub, suffix, ctx);
|
|
589
|
+
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, sub, suffix, ctx);
|
|
684
590
|
if (valueChecks.length === 0)
|
|
685
591
|
continue;
|
|
686
592
|
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
@@ -692,7 +598,7 @@ const generatePatternAndAdditionalChecks = (schema, suffix, ctx) => {
|
|
|
692
598
|
if (hasAdditionalProperties(schema) && isSchemaObject(schema.additionalProperties)) {
|
|
693
599
|
const additional = schema.additionalProperties;
|
|
694
600
|
const kv = `_ak${d}`;
|
|
695
|
-
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${${kv}}\``, additional, suffix, ctx);
|
|
601
|
+
const valueChecks = generateValueChecks(`\${${kv}}`, `${obj}[${kv}]`, `\`${ctx.pathPrefix}/\${escapePointer(${kv})}\``, additional, suffix, ctx);
|
|
696
602
|
if (valueChecks.length > 0) {
|
|
697
603
|
const known = Object.keys(hasProperties(schema) ? schema.properties : {});
|
|
698
604
|
lines.push(` for (const ${kv} in ${obj}) {`);
|
|
@@ -746,10 +652,10 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
|
|
|
746
652
|
];
|
|
747
653
|
};
|
|
748
654
|
const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
|
|
749
|
-
const at = `\`${ctx.pathPrefix}/\${_name}\``;
|
|
655
|
+
const at = `\`${ctx.pathPrefix}/\${escapePointer(_name)}\``;
|
|
750
656
|
const nameCtx = {
|
|
751
657
|
objVar: ctx.objVar,
|
|
752
|
-
pathPrefix: `${ctx.pathPrefix}/\${_name}`,
|
|
658
|
+
pathPrefix: `${ctx.pathPrefix}/\${escapePointer(_name)}`,
|
|
753
659
|
depth: ctx.depth + 1,
|
|
754
660
|
hoisted: ctx.hoisted,
|
|
755
661
|
rootSchema: ctx.rootSchema
|
|
@@ -777,6 +683,10 @@ const generateDependentRequiredChecks = (schema, ctx) => {
|
|
|
777
683
|
}
|
|
778
684
|
return lines;
|
|
779
685
|
};
|
|
686
|
+
const dependentSelfBinding = (objVar, depth) => {
|
|
687
|
+
const name = `_dep${depth}`;
|
|
688
|
+
return { name, declaration: ` const ${name}: unknown = ${objVar}` };
|
|
689
|
+
};
|
|
780
690
|
const generateDependentSchemasChecks = (schema, suffix, ctx) => {
|
|
781
691
|
if (!isSchemaObject(schema))
|
|
782
692
|
return [];
|
|
@@ -799,10 +709,12 @@ const generateDependentSchemasChecks = (schema, suffix, ctx) => {
|
|
|
799
709
|
}
|
|
800
710
|
if (!isSchemaObject(sub))
|
|
801
711
|
continue;
|
|
802
|
-
const
|
|
712
|
+
const self = dependentSelfBinding(obj, ctx.depth);
|
|
713
|
+
const checks = generateValueChecks("", self.name, objPath, sub, suffix, ctx);
|
|
803
714
|
if (checks.length === 0)
|
|
804
715
|
continue;
|
|
805
716
|
lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
|
|
717
|
+
lines.push(` ${self.declaration}`);
|
|
806
718
|
lines.push(...checks.map((line) => ` ${line}`));
|
|
807
719
|
lines.push(` }`);
|
|
808
720
|
}
|
|
@@ -841,10 +753,12 @@ const generateDependenciesChecks = (schema, suffix, ctx) => {
|
|
|
841
753
|
}
|
|
842
754
|
if (!isSchemaObject(value))
|
|
843
755
|
continue;
|
|
844
|
-
const
|
|
756
|
+
const self = dependentSelfBinding(obj, ctx.depth);
|
|
757
|
+
const checks = generateValueChecks("", self.name, objPath, value, suffix, ctx);
|
|
845
758
|
if (checks.length === 0)
|
|
846
759
|
continue;
|
|
847
760
|
lines.push(` if (${hasOwnCheck(obj, trigger)}) {`);
|
|
761
|
+
lines.push(` ${self.declaration}`);
|
|
848
762
|
lines.push(...checks.map((line) => ` ${line}`));
|
|
849
763
|
lines.push(` }`);
|
|
850
764
|
}
|
|
@@ -993,15 +907,17 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
993
907
|
if (hasPropertyNames(schema)) {
|
|
994
908
|
propertyLines.push(...generatePropertyNameChecks(schema.propertyNames, suffix, ctx));
|
|
995
909
|
}
|
|
996
|
-
|
|
910
|
+
const objectCombinators = generateCombinatorChecks("", "_root", "`${_path}`", schema, suffix, ctx);
|
|
911
|
+
if (objectCombinators.length > 0) {
|
|
912
|
+
propertyLines.push(` const _root: unknown = input`);
|
|
913
|
+
propertyLines.push(...objectCombinators);
|
|
914
|
+
}
|
|
997
915
|
const body = (propertyLines.length > 0 ? "\n" + propertyLines.join("\n") + "\n" : "").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
998
|
-
const hoistedBlock = ctx.hoisted.length > 0 ? `${ctx.hoisted.join("\n")}
|
|
999
|
-
|
|
1000
|
-
` : "";
|
|
1001
916
|
const guard = guardObjectConditions(schema, "input", "obj");
|
|
917
|
+
const objBinding = readsObjBinding(body) ? [` const obj = input as Record<string, unknown>`] : [];
|
|
1002
918
|
const collectBody = (name, exported) => [
|
|
1003
919
|
`${exported ? "export " : ""}const ${name} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1004
|
-
|
|
920
|
+
...objBinding,
|
|
1005
921
|
` if (typeof input !== 'object' || input === null || Array.isArray(input)) {`,
|
|
1006
922
|
` return { valid: false, errors: [{ message: 'must be object', path: _path }] }`,
|
|
1007
923
|
` }`,
|
|
@@ -1012,14 +928,15 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1012
928
|
`}`
|
|
1013
929
|
].join("\n");
|
|
1014
930
|
if (!guard) {
|
|
1015
|
-
return
|
|
931
|
+
return withHoisted(ctx.hoisted, collectBody(vName, true));
|
|
1016
932
|
}
|
|
933
|
+
const guardText = guard.join("\n");
|
|
1017
934
|
const collectName = `${vName}Errors`;
|
|
1018
|
-
return [
|
|
1019
|
-
|
|
935
|
+
return withHoisted(ctx.hoisted, [
|
|
936
|
+
collectBody(collectName, false),
|
|
1020
937
|
``,
|
|
1021
938
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1022
|
-
` const obj = input as Record<string, unknown
|
|
939
|
+
...readsObjBinding(guardText) ? [` const obj = input as Record<string, unknown>`] : [],
|
|
1023
940
|
` if (`,
|
|
1024
941
|
guard.map((condition) => ` ${condition}`).join(" &&\n"),
|
|
1025
942
|
` ) {`,
|
|
@@ -1027,21 +944,49 @@ const generateObjectValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1027
944
|
` }`,
|
|
1028
945
|
` return ${collectName}(input, _path)`,
|
|
1029
946
|
`}`
|
|
1030
|
-
].join("\n");
|
|
947
|
+
].join("\n"));
|
|
1031
948
|
};
|
|
1032
949
|
const guardName = (typeName) => `is${typeName}`;
|
|
1033
|
-
const
|
|
950
|
+
const valueCanHaveType = (value, type) => {
|
|
951
|
+
switch (type) {
|
|
952
|
+
case "string":
|
|
953
|
+
return typeof value === "string";
|
|
954
|
+
case "number":
|
|
955
|
+
return typeof value === "number";
|
|
956
|
+
case "integer":
|
|
957
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
958
|
+
case "boolean":
|
|
959
|
+
return typeof value === "boolean";
|
|
960
|
+
case "null":
|
|
961
|
+
return value === null;
|
|
962
|
+
case "array":
|
|
963
|
+
return Array.isArray(value);
|
|
964
|
+
case "object":
|
|
965
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
966
|
+
default:
|
|
967
|
+
return true;
|
|
968
|
+
}
|
|
969
|
+
};
|
|
970
|
+
const booleanLeafExpr = (schema, acc, narrowable = true) => {
|
|
1034
971
|
if (!isSchemaObject(schema))
|
|
1035
972
|
return null;
|
|
1036
|
-
|
|
973
|
+
const typed = (type) => narrowable ? acc : `(${acc} as ${type})`;
|
|
974
|
+
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 || // A draft-07 tuple: the fixed positions live in an *array* `items` and the
|
|
975
|
+
// tail in `additionalItems`. Neither is expressible flat, and reading past
|
|
976
|
+
// them let the guard accept tuples `validateX` rejects.
|
|
977
|
+
Array.isArray(schema["items"]) || "additionalItems" in schema || carriesUnevaluated(schema) || getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0) {
|
|
1037
978
|
return null;
|
|
1038
979
|
}
|
|
1039
|
-
if (
|
|
980
|
+
if (!hasType(schema)) {
|
|
981
|
+
if (!hasEnum(schema))
|
|
982
|
+
return null;
|
|
983
|
+
if (declaresKeywordOutside(schema, ["enum"]))
|
|
984
|
+
return null;
|
|
1040
985
|
return enumMembershipExpr(schema.enum, acc);
|
|
1041
986
|
}
|
|
1042
|
-
if (!hasType(schema))
|
|
1043
|
-
return null;
|
|
1044
987
|
const t = schema.type;
|
|
988
|
+
const membership = hasEnum(schema) ? enumMembershipExpr(schema.enum.filter((member) => valueCanHaveType(member, t)), acc) : null;
|
|
989
|
+
const withMembership = (expr) => expr === null || membership === null ? expr : `${expr} && ${membership}`;
|
|
1045
990
|
switch (t) {
|
|
1046
991
|
// Each constraint is the exact negation of the validator's error condition,
|
|
1047
992
|
// so edge values — `NaN` above all, which compares `false` against every
|
|
@@ -1050,56 +995,61 @@ const booleanLeafExpr = (schema, acc) => {
|
|
|
1050
995
|
// condition is its negation; the length checks come from the same
|
|
1051
996
|
// `string-length-check` emitter the validator uses, for the same reason.
|
|
1052
997
|
case "string": {
|
|
998
|
+
const str = typed("string");
|
|
1053
999
|
const parts = [`typeof ${acc} === 'string'`];
|
|
1054
1000
|
if (hasPattern(schema))
|
|
1055
|
-
parts.push(`${regexLiteral(schema.pattern)}.test(${
|
|
1001
|
+
parts.push(`${regexLiteral(schema.pattern)}.test(${str})`);
|
|
1056
1002
|
if (hasMinLength(schema))
|
|
1057
|
-
parts.push(minLengthPassExpr(
|
|
1003
|
+
parts.push(minLengthPassExpr(str, schema.minLength));
|
|
1058
1004
|
if (hasMaxLength(schema))
|
|
1059
|
-
parts.push(maxLengthPassExpr(
|
|
1060
|
-
return parts.join(" && ");
|
|
1005
|
+
parts.push(maxLengthPassExpr(str, schema.maxLength));
|
|
1006
|
+
return withMembership(parts.join(" && "));
|
|
1061
1007
|
}
|
|
1062
1008
|
case "number":
|
|
1063
1009
|
case "integer": {
|
|
1010
|
+
const num = typed("number");
|
|
1064
1011
|
const parts = [`typeof ${acc} === 'number'`];
|
|
1065
1012
|
if (t === "integer")
|
|
1066
1013
|
parts.push(`Number.isInteger(${acc})`);
|
|
1067
1014
|
if (hasMinimum(schema))
|
|
1068
|
-
parts.push(`${
|
|
1015
|
+
parts.push(`${num} ${hasStrictExclusiveMinimum(schema) ? ">" : ">="} ${schema.minimum}`);
|
|
1069
1016
|
if (hasMaximum(schema))
|
|
1070
|
-
parts.push(`${
|
|
1017
|
+
parts.push(`${num} ${hasStrictExclusiveMaximum(schema) ? "<" : "<="} ${schema.maximum}`);
|
|
1071
1018
|
if (hasExclusiveMinimum(schema))
|
|
1072
|
-
parts.push(`${
|
|
1019
|
+
parts.push(`${num} > ${schema.exclusiveMinimum}`);
|
|
1073
1020
|
if (hasExclusiveMaximum(schema))
|
|
1074
|
-
parts.push(`${
|
|
1021
|
+
parts.push(`${num} < ${schema.exclusiveMaximum}`);
|
|
1075
1022
|
if (hasMultipleOf(schema))
|
|
1076
|
-
parts.push(multipleOfPassExpr(
|
|
1077
|
-
return parts.join(" && ");
|
|
1023
|
+
parts.push(multipleOfPassExpr(num, schema.multipleOf));
|
|
1024
|
+
return withMembership(parts.join(" && "));
|
|
1078
1025
|
}
|
|
1079
1026
|
case "boolean":
|
|
1080
|
-
return `typeof ${acc} === 'boolean'
|
|
1027
|
+
return withMembership(`typeof ${acc} === 'boolean'`);
|
|
1081
1028
|
case "null":
|
|
1082
|
-
return `${acc} === null
|
|
1029
|
+
return withMembership(`${acc} === null`);
|
|
1083
1030
|
case "object": {
|
|
1084
|
-
const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)
|
|
1085
|
-
return parts === null ? null : parts.join(" && ");
|
|
1031
|
+
const parts = booleanObjectParts(schema, acc, `(${acc} as Record<string, unknown>)`, false);
|
|
1032
|
+
return parts === null ? null : withMembership(parts.join(" && "));
|
|
1086
1033
|
}
|
|
1087
1034
|
case "array":
|
|
1088
|
-
return booleanArrayExpr(schema, acc);
|
|
1035
|
+
return withMembership(booleanArrayExpr(schema, acc, narrowable));
|
|
1089
1036
|
default:
|
|
1090
1037
|
return null;
|
|
1091
1038
|
}
|
|
1092
1039
|
};
|
|
1093
|
-
const booleanArrayExpr = (schema, acc) => {
|
|
1040
|
+
const booleanArrayExpr = (schema, acc, narrowable = true) => {
|
|
1041
|
+
const arr = narrowable ? acc : `(${acc} as unknown[])`;
|
|
1094
1042
|
const parts = [`Array.isArray(${acc})`];
|
|
1095
1043
|
if (hasMinItems(schema))
|
|
1096
|
-
parts.push(`${
|
|
1044
|
+
parts.push(`${arr}.length >= ${schema.minItems}`);
|
|
1097
1045
|
if (hasMaxItems(schema))
|
|
1098
|
-
parts.push(`${
|
|
1046
|
+
parts.push(`${arr}.length <= ${schema.maxItems}`);
|
|
1099
1047
|
if (hasUniqueItems(schema) && schema.uniqueItems === true) {
|
|
1100
|
-
parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${
|
|
1048
|
+
parts.push(arrayItemsAreScalarOnly(schema) ? `new Set((${acc} as unknown[]).map((_u) => JSON.stringify(_u))).size === ${arr}.length` : `allUnique(${acc} as unknown[])`);
|
|
1101
1049
|
}
|
|
1102
1050
|
const base = parts.join(" && ");
|
|
1051
|
+
if (schema["items"] === false)
|
|
1052
|
+
return `${base} && ${arr}.length === 0`;
|
|
1103
1053
|
if (!hasItems(schema))
|
|
1104
1054
|
return base;
|
|
1105
1055
|
const items = schema.items;
|
|
@@ -1112,9 +1062,13 @@ const booleanArrayExpr = (schema, acc) => {
|
|
|
1112
1062
|
return null;
|
|
1113
1063
|
return `${base} && Array.from(${acc} as unknown[]).every((_it) => (${itemExpr}))`;
|
|
1114
1064
|
};
|
|
1115
|
-
const booleanObjectParts = (schema, raw, objAcc) => {
|
|
1065
|
+
const booleanObjectParts = (schema, raw, objAcc, narrowable = true) => {
|
|
1116
1066
|
if (!isObjectSchema(schema))
|
|
1117
1067
|
return null;
|
|
1068
|
+
if (hasRef(schema) || hasConst(schema) || hasEnum(schema))
|
|
1069
|
+
return null;
|
|
1070
|
+
if (getMjstInstanceOf(schema) !== void 0 || getMjstPrimitive(schema) !== void 0)
|
|
1071
|
+
return null;
|
|
1118
1072
|
if (hasDependentRequired(schema) || hasPropertyNames(schema) || "dependentSchemas" in schema)
|
|
1119
1073
|
return null;
|
|
1120
1074
|
if (hasMinProperties(schema) || hasMaxProperties(schema) || "dependencies" in schema)
|
|
@@ -1144,11 +1098,16 @@ const booleanObjectParts = (schema, raw, objAcc) => {
|
|
|
1144
1098
|
if (PROTOTYPE_MEMBERS.has(key))
|
|
1145
1099
|
return null;
|
|
1146
1100
|
const member = safeAccessor(objAcc, key);
|
|
1147
|
-
const expr = booleanLeafExpr(propSchema, member);
|
|
1101
|
+
const expr = booleanLeafExpr(propSchema, member, narrowable);
|
|
1148
1102
|
if (expr === null)
|
|
1149
1103
|
return null;
|
|
1150
1104
|
parts.push(required.has(key) ? expr : `(${member} === undefined || (${expr}))`);
|
|
1151
1105
|
}
|
|
1106
|
+
for (const key of required) {
|
|
1107
|
+
if (Object.hasOwn(properties, key))
|
|
1108
|
+
continue;
|
|
1109
|
+
parts.push(hasOwnCheck(objAcc, key));
|
|
1110
|
+
}
|
|
1152
1111
|
if (strict) {
|
|
1153
1112
|
if (keys.length === 0) {
|
|
1154
1113
|
parts.push(`Object.keys(${objAcc}).length === 0`);
|
|
@@ -1182,13 +1141,15 @@ const generateBooleanGuard = (schema, typeName, _suffix = "") => {
|
|
|
1182
1141
|
const returns = typeDescribesEveryAcceptedValue(rewriteNullable(schema)) ? `input is ${typeName}` : "boolean";
|
|
1183
1142
|
const fallback = `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`;
|
|
1184
1143
|
const rewritten = rewriteNullable(schema);
|
|
1185
|
-
if (declaresObjectType(rewritten)) {
|
|
1144
|
+
if (declaresObjectType(rewritten) && objectRootIsSelfContained(rewritten)) {
|
|
1186
1145
|
const parts = booleanObjectParts(rewritten, "input", "obj");
|
|
1187
1146
|
if (parts === null)
|
|
1188
1147
|
return fallback;
|
|
1189
1148
|
return [
|
|
1190
1149
|
`export const ${name} = (input: unknown): ${returns} => {`,
|
|
1191
|
-
|
|
1150
|
+
// Same unused-local as the validator's hot guard: a node with no property
|
|
1151
|
+
// to read guards on the shape alone and never touches the narrowing.
|
|
1152
|
+
...readsObjBinding(parts.join("\n")) ? [` const obj = input as Record<string, unknown>`] : [],
|
|
1192
1153
|
` return (`,
|
|
1193
1154
|
parts.map((part) => ` ${part}`).join(" &&\n"),
|
|
1194
1155
|
` )`,
|
|
@@ -1212,7 +1173,10 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1212
1173
|
}
|
|
1213
1174
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
|
|
1214
1175
|
}
|
|
1176
|
+
const generalRoot = () => generateGeneralRootValidator(schema, typeName, suffix, rootSchema);
|
|
1215
1177
|
if (hasRef(schema)) {
|
|
1178
|
+
if (declaresKeywordOutside(schema, ["$ref"]))
|
|
1179
|
+
return generalRoot();
|
|
1216
1180
|
const delegateName = validatorName(refToName(schema.$ref, suffix));
|
|
1217
1181
|
return [
|
|
1218
1182
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
@@ -1222,6 +1186,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1222
1186
|
}
|
|
1223
1187
|
const instanceOf = getMjstInstanceOf(schema);
|
|
1224
1188
|
if (instanceOf) {
|
|
1189
|
+
if (declaresKeywordOutside(schema, [MJST_EXTENSION_KEY, "type"]))
|
|
1190
|
+
return generalRoot();
|
|
1225
1191
|
return [
|
|
1226
1192
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1227
1193
|
` if (!(input instanceof ${instanceOf})) {`,
|
|
@@ -1233,6 +1199,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1233
1199
|
}
|
|
1234
1200
|
const primitive = getMjstPrimitive(schema);
|
|
1235
1201
|
if (primitive) {
|
|
1202
|
+
if (declaresKeywordOutside(schema, [MJST_EXTENSION_KEY, "type"]))
|
|
1203
|
+
return generalRoot();
|
|
1236
1204
|
return [
|
|
1237
1205
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1238
1206
|
` if (typeof input !== "${primitive}") {`,
|
|
@@ -1243,6 +1211,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1243
1211
|
].join("\n");
|
|
1244
1212
|
}
|
|
1245
1213
|
if (hasConst(schema)) {
|
|
1214
|
+
if (declaresKeywordOutside(schema, ["const"]))
|
|
1215
|
+
return generalRoot();
|
|
1246
1216
|
const mismatch = constMismatchCondition("input", schema.const);
|
|
1247
1217
|
const msg = JSON.stringify(`must be ${JSON.stringify(schema.const)}`);
|
|
1248
1218
|
return [
|
|
@@ -1255,6 +1225,8 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1255
1225
|
].join("\n");
|
|
1256
1226
|
}
|
|
1257
1227
|
if (hasEnum(schema)) {
|
|
1228
|
+
if (declaresKeywordOutside(schema, ["enum"]))
|
|
1229
|
+
return generalRoot();
|
|
1258
1230
|
const label = schema.enum.map((v) => JSON.stringify(v)).join(", ");
|
|
1259
1231
|
return [
|
|
1260
1232
|
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
@@ -1290,16 +1262,13 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1290
1262
|
checks.push(...generateConstraintChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1291
1263
|
checks.push(...generateCombinatorChecks("", "input", rootPath, schema, suffix, ctx));
|
|
1292
1264
|
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
` : "";
|
|
1296
|
-
return [
|
|
1297
|
-
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1265
|
+
return withHoisted(ctx.hoisted, [
|
|
1266
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1298
1267
|
` let errors: ValidationError[] | undefined`,
|
|
1299
1268
|
body,
|
|
1300
1269
|
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1301
1270
|
`}`
|
|
1302
|
-
].join("\n");
|
|
1271
|
+
].join("\n"));
|
|
1303
1272
|
}
|
|
1304
1273
|
const rootTypeArray = getTypeArray(schema);
|
|
1305
1274
|
if (rootTypeArray) {
|
|
@@ -1322,23 +1291,20 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1322
1291
|
].join("\n");
|
|
1323
1292
|
}
|
|
1324
1293
|
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
` : "";
|
|
1328
|
-
return [
|
|
1329
|
-
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1294
|
+
return withHoisted(ctx.hoisted, [
|
|
1295
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1330
1296
|
` let errors: ValidationError[] | undefined`,
|
|
1331
1297
|
body,
|
|
1332
1298
|
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1333
1299
|
`}`
|
|
1334
|
-
].join("\n");
|
|
1300
|
+
].join("\n"));
|
|
1335
1301
|
}
|
|
1336
1302
|
if (hasType(schema)) {
|
|
1337
1303
|
const t = schema.type;
|
|
1338
1304
|
const wrongType = wrongTypeCondition("input", t);
|
|
1339
1305
|
const typLabel = typeofString(t);
|
|
1340
1306
|
const rootCtx = createRootContext(rootSchema);
|
|
1341
|
-
const constraintLines = generateConstraintChecks("", "
|
|
1307
|
+
const constraintLines = generateConstraintChecks("", "_root", "`${_path}`", schema, suffix, rootCtx);
|
|
1342
1308
|
if (!wrongType) {
|
|
1343
1309
|
return [
|
|
1344
1310
|
`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
|
|
@@ -1356,50 +1322,42 @@ const generateScalarValidator = (schema, typeName, suffix, rootSchema) => {
|
|
|
1356
1322
|
`}`
|
|
1357
1323
|
].join("\n");
|
|
1358
1324
|
}
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
` : "";
|
|
1362
|
-
return [
|
|
1363
|
-
`${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1325
|
+
return withHoisted(rootCtx.hoisted, [
|
|
1326
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1364
1327
|
` if (${wrongType}) {`,
|
|
1365
1328
|
` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
|
|
1366
1329
|
` }`,
|
|
1330
|
+
` const _root: unknown = input`,
|
|
1367
1331
|
` let errors: ValidationError[] | undefined`,
|
|
1368
1332
|
constraintLines.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
|
|
1369
1333
|
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1370
1334
|
`}`
|
|
1371
|
-
].join("\n");
|
|
1335
|
+
].join("\n"));
|
|
1372
1336
|
}
|
|
1373
1337
|
const typelessCtx = createRootContext(rootSchema);
|
|
1374
1338
|
const typelessChecks = generateConstraintChecks("", "input", "`${_path}`", schema, suffix, typelessCtx);
|
|
1375
1339
|
if (typelessChecks.length === 0) {
|
|
1376
1340
|
return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join("\n");
|
|
1377
1341
|
}
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
` : "";
|
|
1381
|
-
return [
|
|
1382
|
-
`${typelessHoisted}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1342
|
+
return withHoisted(typelessCtx.hoisted, [
|
|
1343
|
+
`export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1383
1344
|
` let errors: ValidationError[] | undefined`,
|
|
1384
1345
|
typelessChecks.join("\n").replaceAll("errors.push(", "(errors ??= []).push("),
|
|
1385
1346
|
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1386
1347
|
`}`
|
|
1387
|
-
].join("\n");
|
|
1348
|
+
].join("\n"));
|
|
1388
1349
|
};
|
|
1389
|
-
const
|
|
1350
|
+
const generateGeneralRootValidator = (schema, typeName, suffix, rootSchema) => {
|
|
1390
1351
|
const ctx = createRootContext(rootSchema);
|
|
1391
1352
|
const checks = generateValueChecks("", "input", "`${_path}`", schema, suffix, ctx, true);
|
|
1392
1353
|
const body = checks.join("\n").replaceAll("errors.push(", "(errors ??= []).push(");
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
` : "";
|
|
1396
|
-
return [
|
|
1397
|
-
`${hoistedBlock}export const ${validatorName(typeName)} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1354
|
+
return withHoisted(ctx.hoisted, [
|
|
1355
|
+
`export const ${validatorName(typeName)} = (input: unknown, _path = ''): ValidationResult => {`,
|
|
1398
1356
|
` let errors: ValidationError[] | undefined`,
|
|
1399
1357
|
body,
|
|
1400
1358
|
` return errors !== undefined ? { valid: false, errors } : true`,
|
|
1401
1359
|
`}`
|
|
1402
|
-
].join("\n");
|
|
1360
|
+
].join("\n"));
|
|
1403
1361
|
};
|
|
1404
1362
|
const SINGLE_SUBSCHEMA_KEYS = /* @__PURE__ */ new Set([
|
|
1405
1363
|
"additionalProperties",
|
|
@@ -1462,9 +1420,9 @@ const generateValidatorFunction = (schema, typeName, suffix = "", rootSchema) =>
|
|
|
1462
1420
|
const document = rootSchema ?? schema;
|
|
1463
1421
|
assertUnevaluatedGeneratable(rewritten, typeName, document, unevaluatedMatcher(suffix, createRootContext(document)));
|
|
1464
1422
|
if (carriesUnevaluated(rewritten)) {
|
|
1465
|
-
return
|
|
1423
|
+
return generateGeneralRootValidator(rewritten, typeName, suffix, document);
|
|
1466
1424
|
}
|
|
1467
|
-
if (declaresObjectType(rewritten)) {
|
|
1425
|
+
if (declaresObjectType(rewritten) && objectRootIsSelfContained(rewritten)) {
|
|
1468
1426
|
return generateObjectValidator(rewritten, typeName, suffix, document);
|
|
1469
1427
|
}
|
|
1470
1428
|
return generateScalarValidator(rewritten, typeName, suffix, document);
|