@amritk/generate-parsers 0.18.0 → 0.19.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.
@@ -1,14 +1,116 @@
1
- import { escapeRegexPattern } from "@amritk/helpers/escape-regex-pattern";
1
+ import { regexLiteral } from "@amritk/helpers/escape-regex-pattern";
2
2
  import { getMjstInstanceOf, getMjstPrimitive } from "@amritk/helpers/mjst-extension";
3
3
  import { multipleOfFailExpr } from "@amritk/helpers/multiple-of-check";
4
4
  import { quoteJsString } from "@amritk/helpers/quote-js-string";
5
5
  import { resolveRef } from "@amritk/helpers/resolve-ref";
6
- import { safeAccessor } from "@amritk/helpers/safe-accessor";
7
- import { hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
6
+ import { hasOwnCheck, missingCheck, safeAccessor } from "@amritk/helpers/safe-accessor";
7
+ import { hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMaxProperties, hasMinItems, hasMinimum, hasMinLength, hasMinProperties, hasMultipleOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasType, hasUniqueItems, isSchemaObject } from "@amritk/helpers/schema-guards";
8
+ import { maxLengthFailExpr, minLengthFailExpr } from "@amritk/helpers/string-length-check";
9
+ import { generateDeepEqualCheck } from "./generate-deep-equal-check.js";
8
10
  import { generateEnumCheck } from "./generate-enum-check.js";
9
- import { canEnforceUnion, generateUnionCheck, getUnionBranches, isInlineObjectProperty, multiTypeCheck } from "./generate-type-checks.js";
10
- import { getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
11
- import { subschemaMatchExpr } from "./subschema-match.js";
11
+ import { canEnforceUnion, generateUnionCheck, getUnionBranches, isExclusiveUnion, isInlineObjectProperty, multiTypeCheck } from "./generate-type-checks.js";
12
+ import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
13
+ import { everyTailItem, getPrefixItems, prefixItemsCapsLength, scalarItemTypeCheck } from "./generate-validation-expression.js";
14
+ import { hasConstrainingRefSibling, subschemaMatchExpr } from "./subschema-match.js";
15
+ const matchContext = (context) => context.rootSchema ? { rootSchema: context.rootSchema } : {};
16
+ const TYPELESS_CONSTRAINT_KEYWORDS = [
17
+ "minimum",
18
+ "maximum",
19
+ "exclusiveMinimum",
20
+ "exclusiveMaximum",
21
+ "multipleOf",
22
+ "minLength",
23
+ "maxLength",
24
+ "pattern",
25
+ "minItems",
26
+ "maxItems",
27
+ "uniqueItems",
28
+ "items",
29
+ "prefixItems",
30
+ "contains"
31
+ ];
32
+ const TYPELESS_OBJECT_KEYWORDS = [
33
+ "required",
34
+ "minProperties",
35
+ "maxProperties",
36
+ "propertyNames",
37
+ "patternProperties",
38
+ "additionalProperties",
39
+ "dependentRequired",
40
+ "dependentSchemas"
41
+ ];
42
+ const backstopReason = (schema, context) => {
43
+ if (!isSchemaObject(schema))
44
+ return null;
45
+ const record = schema;
46
+ if ("unevaluatedProperties" in record && record["unevaluatedProperties"] !== true)
47
+ return "unevaluatedProperties";
48
+ if ("unevaluatedItems" in record && record["unevaluatedItems"] !== true)
49
+ return "unevaluatedItems";
50
+ if (hasRef(schema) && (!context.useRefImports || hasConstrainingRefSibling(schema)))
51
+ return "$ref";
52
+ if (record["items"] === false && !Array.isArray(record["prefixItems"]))
53
+ return "items";
54
+ if (!("type" in record)) {
55
+ if (TYPELESS_CONSTRAINT_KEYWORDS.some((keyword) => keyword in record))
56
+ return "constraints";
57
+ if (!("properties" in record) && TYPELESS_OBJECT_KEYWORDS.some((keyword) => keyword in record))
58
+ return "constraints";
59
+ return null;
60
+ }
61
+ if (Array.isArray(record["type"])) {
62
+ const nonNull = record["type"].filter((type) => type !== "null");
63
+ const constrains = TYPELESS_CONSTRAINT_KEYWORDS.some((keyword) => keyword in record) || TYPELESS_OBJECT_KEYWORDS.some((keyword) => keyword in record);
64
+ if (nonNull.length > 1 && constrains)
65
+ return "constraints";
66
+ }
67
+ return null;
68
+ };
69
+ const backstopSchema = (schema, reason) => {
70
+ if (reason !== "constraints" && reason !== "items")
71
+ return schema;
72
+ const record = schema;
73
+ const residual = {};
74
+ for (const keyword of [
75
+ "type",
76
+ ...TYPELESS_CONSTRAINT_KEYWORDS,
77
+ ...TYPELESS_OBJECT_KEYWORDS,
78
+ "minContains",
79
+ "maxContains"
80
+ ]) {
81
+ if (keyword in record)
82
+ residual[keyword] = record[keyword];
83
+ }
84
+ if ("additionalProperties" in residual && "properties" in record) {
85
+ const properties = record["properties"];
86
+ if (typeof properties === "object" && properties !== null && !Array.isArray(properties)) {
87
+ residual["properties"] = Object.fromEntries(Object.keys(properties).map((k) => [k, true]));
88
+ }
89
+ }
90
+ return residual;
91
+ };
92
+ const backstopMessage = (reason, label) => {
93
+ switch (reason) {
94
+ case "unevaluatedProperties":
95
+ return `${label} must NOT have unevaluated properties`;
96
+ case "unevaluatedItems":
97
+ return `${label} must NOT have unevaluated items`;
98
+ case "$ref":
99
+ return `${label} does not satisfy the referenced schema`;
100
+ default:
101
+ return `${label} does not satisfy the schema`;
102
+ }
103
+ };
104
+ const generateBackstopAssertion = (acc, schema, label, context, optional = false) => {
105
+ const reason = backstopReason(schema, context);
106
+ if (reason === null)
107
+ return [];
108
+ const match = subschemaMatchExpr(acc, backstopSchema(schema, reason), matchContext(context));
109
+ if (match === null || match === "true")
110
+ return [];
111
+ const failure = throwError(backstopMessage(reason, label));
112
+ return [optional ? ` if (${acc} !== undefined && !(${match})) ${failure};` : ` if (!(${match})) ${failure};`];
113
+ };
12
114
  const wrongTypeCondition = (accessor, type) => {
13
115
  switch (type) {
14
116
  case "string":
@@ -41,28 +143,28 @@ const generateConstraintChecks = (acc, propSchema, field, context = {}) => {
41
143
  const lines = [];
42
144
  if (t === "string") {
43
145
  if (hasPattern(propSchema)) {
44
- const pattern = escapeRegexPattern(propSchema.pattern);
45
- lines.push(` if (typeof ${acc} === "string" && !/${pattern}/.test(${acc})) ${throwError(`${field} must match pattern ${propSchema.pattern}`)};`);
146
+ const patternLiteral = regexLiteral(propSchema.pattern);
147
+ lines.push(` if (typeof ${acc} === "string" && !${patternLiteral}.test(${acc})) ${throwError(`${field} must match pattern ${propSchema.pattern}`)};`);
46
148
  }
47
149
  if (hasMinLength(propSchema)) {
48
- lines.push(` if (typeof ${acc} === "string" && ${acc}.length < ${propSchema.minLength}) ${throwError(`${field} must have at least ${propSchema.minLength} characters`)};`);
150
+ lines.push(` if (typeof ${acc} === "string" && ${minLengthFailExpr(acc, propSchema.minLength)}) ${throwError(`${field} must have at least ${propSchema.minLength} characters`)};`);
49
151
  }
50
152
  if (hasMaxLength(propSchema)) {
51
- lines.push(` if (typeof ${acc} === "string" && ${acc}.length > ${propSchema.maxLength}) ${throwError(`${field} must have at most ${propSchema.maxLength} characters`)};`);
153
+ lines.push(` if (typeof ${acc} === "string" && ${maxLengthFailExpr(acc, propSchema.maxLength)}) ${throwError(`${field} must have at most ${propSchema.maxLength} characters`)};`);
52
154
  }
53
155
  }
54
156
  if (t === "number" || t === "integer") {
55
157
  if (hasMinimum(propSchema)) {
56
- lines.push(` if (typeof ${acc} === "number" && ${acc} < ${propSchema.minimum}) ${throwError(`${field} must be >= ${propSchema.minimum}`)};`);
158
+ lines.push(` if (typeof ${acc} === "number" && !(${acc} >= ${propSchema.minimum})) ${throwError(`${field} must be >= ${propSchema.minimum}`)};`);
57
159
  }
58
160
  if (hasMaximum(propSchema)) {
59
- lines.push(` if (typeof ${acc} === "number" && ${acc} > ${propSchema.maximum}) ${throwError(`${field} must be <= ${propSchema.maximum}`)};`);
161
+ lines.push(` if (typeof ${acc} === "number" && !(${acc} <= ${propSchema.maximum})) ${throwError(`${field} must be <= ${propSchema.maximum}`)};`);
60
162
  }
61
163
  if (hasExclusiveMinimum(propSchema)) {
62
- lines.push(` if (typeof ${acc} === "number" && ${acc} <= ${propSchema.exclusiveMinimum}) ${throwError(`${field} must be > ${propSchema.exclusiveMinimum}`)};`);
164
+ lines.push(` if (typeof ${acc} === "number" && !(${acc} > ${propSchema.exclusiveMinimum})) ${throwError(`${field} must be > ${propSchema.exclusiveMinimum}`)};`);
63
165
  }
64
166
  if (hasExclusiveMaximum(propSchema)) {
65
- lines.push(` if (typeof ${acc} === "number" && ${acc} >= ${propSchema.exclusiveMaximum}) ${throwError(`${field} must be < ${propSchema.exclusiveMaximum}`)};`);
167
+ lines.push(` if (typeof ${acc} === "number" && !(${acc} < ${propSchema.exclusiveMaximum})) ${throwError(`${field} must be < ${propSchema.exclusiveMaximum}`)};`);
66
168
  }
67
169
  if (hasMultipleOf(propSchema)) {
68
170
  lines.push(` if (typeof ${acc} === "number" && ${multipleOfFailExpr(acc, propSchema.multipleOf)}) ${throwError(`${field} must be a multiple of ${propSchema.multipleOf}`)};`);
@@ -76,17 +178,33 @@ const generateConstraintChecks = (acc, propSchema, field, context = {}) => {
76
178
  lines.push(` if (Array.isArray(${acc}) && ${acc}.length > ${propSchema.maxItems}) ${throwError(`${field} must have at most ${propSchema.maxItems} items`)};`);
77
179
  }
78
180
  if (hasUniqueItems(propSchema) && propSchema.uniqueItems === true) {
79
- lines.push(` if (Array.isArray(${acc}) && new Set(${acc}.map((_u) => JSON.stringify(_u))).size !== ${acc}.length) ${throwError(`${field} must NOT have duplicate items`)};`);
181
+ lines.push(` if (Array.isArray(${acc}) && !(${generateUniqueItemsCheck(acc, propSchema)})) ${throwError(`${field} must NOT have duplicate items`)};`);
80
182
  }
81
- const itemCheck = generateItemCheck(propSchema);
183
+ const itemCheck = generateItemCheck(propSchema, context);
82
184
  if (itemCheck) {
83
- lines.push(` if (Array.isArray(${acc}) && !${acc}.every((_it) => ${itemCheck.check})) ${throwError(`${field} ${itemCheck.message}`)};`);
185
+ lines.push(` if (Array.isArray(${acc}) && !${everyTailItem(acc, itemCheck.check, propSchema)}) ${throwError(`${field} ${itemCheck.message}`)};`);
84
186
  }
85
- lines.push(...generatePrefixItemsAssertion(acc, field, propSchema, context.rootSchema));
187
+ lines.push(...generatePrefixItemsAssertion(acc, field, propSchema, context));
188
+ }
189
+ if (t === "object") {
190
+ lines.push(...generateObjectSizeChecks(acc, propSchema, field, true));
191
+ }
192
+ return lines;
193
+ };
194
+ const generateObjectSizeChecks = (acc, schema, label, guard) => {
195
+ if (!isSchemaObject(schema))
196
+ return [];
197
+ const lines = [];
198
+ const prefix = guard ? `isObject(${acc}) && ` : "";
199
+ if (hasMinProperties(schema)) {
200
+ lines.push(` if (${prefix}Object.keys(${acc}).length < ${schema.minProperties}) ${throwError(`${label} must have at least ${schema.minProperties} properties`)};`);
201
+ }
202
+ if (hasMaxProperties(schema)) {
203
+ lines.push(` if (${prefix}Object.keys(${acc}).length > ${schema.maxProperties}) ${throwError(`${label} must have at most ${schema.maxProperties} properties`)};`);
86
204
  }
87
205
  return lines;
88
206
  };
89
- const generateItemCheck = (schema) => {
207
+ const generateItemCheck = (schema, context) => {
90
208
  if (!isSchemaObject(schema) || !hasItems(schema) || Array.isArray(schema.items))
91
209
  return null;
92
210
  const items = schema.items;
@@ -95,17 +213,175 @@ const generateItemCheck = (schema) => {
95
213
  return { check: generateEnumCheck("_it", items.enum), message: `items must be one of: ${label}` };
96
214
  }
97
215
  const scalarCheck = scalarItemTypeCheck(items, "_it");
98
- if (scalarCheck === null || !isSchemaObject(items) || !hasType(items))
216
+ if (scalarCheck !== null && isSchemaObject(items) && hasType(items)) {
217
+ return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
218
+ }
219
+ if (hasRef(items) && context.useRefImports === true || isInlineObjectProperty(items))
220
+ return null;
221
+ const match = subschemaMatchExpr("_it", items, matchContext(context));
222
+ if (match === null || match === "true")
223
+ return null;
224
+ return { check: match, message: "items do not match the item schema" };
225
+ };
226
+ const nullableObjectShapeChecks = (acc, schema, type, label, optional, context) => {
227
+ if (type !== "object")
228
+ return [];
229
+ const match = subschemaMatchExpr(acc, { ...schema, type: "object" }, matchContext(context));
230
+ if (match === null || match === "true")
231
+ return [];
232
+ const presence = optional ? `${acc} !== undefined && ${acc} !== null` : `${acc} !== null`;
233
+ return [` if (${presence} && !(${match})) ${throwError(`${label} does not match the declared object shape`)};`];
234
+ };
235
+ const generateNotCheck = (acc, schema, label, optional, context) => {
236
+ if (!isSchemaObject(schema) || !("not" in schema))
237
+ return [];
238
+ const match = subschemaMatchExpr(acc, schema["not"], matchContext(context));
239
+ if (match === null || match === "false")
240
+ return [];
241
+ const failure = throwError(`${label} must NOT match the excluded schema`);
242
+ return [optional ? ` if (${acc} !== undefined && (${match})) ${failure};` : ` if (${match}) ${failure};`];
243
+ };
244
+ const generateAllOfChecks = (acc, schema, label, optional, context, skipInlineMembers = false) => {
245
+ if (!isSchemaObject(schema) || !Array.isArray(schema.allOf))
246
+ return [];
247
+ const handledInline = new Set(skipInlineMembers ? inlineAllOfMembers(schema) : []);
248
+ const lines = [];
249
+ for (const member of schema.allOf) {
250
+ if (handledInline.has(member))
251
+ continue;
252
+ const delegated = context.useRefImports === true && hasProperties(schema) && !hasConstrainingRefSibling(member);
253
+ if (isSchemaObject(member) && hasRef(member) && delegated)
254
+ continue;
255
+ const match = subschemaMatchExpr(acc, member, matchContext(context));
256
+ if (match === null || match === "true")
257
+ continue;
258
+ const failure = throwError(`${label} does not satisfy all of the declared schemas`);
259
+ lines.push(optional ? ` if (${acc} !== undefined && !(${match})) ${failure};` : ` if (!(${match})) ${failure};`);
260
+ }
261
+ return lines;
262
+ };
263
+ const flatUnionCheck = (acc, schema, context) => {
264
+ const branches = getUnionBranches(schema);
265
+ if (branches === null || context.stripUnknown === true)
266
+ return null;
267
+ if (!canEnforceUnion(branches, context.rootSchema))
99
268
  return null;
100
- return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
269
+ return generateUnionCheck(acc, branches, context.useRefImports ?? false, context.suffix ?? "", isExclusiveUnion(schema));
270
+ };
271
+ const generateUnionMatchChecks = (acc, schema, label, optional, context) => {
272
+ if (!isSchemaObject(schema))
273
+ return [];
274
+ const record = schema;
275
+ const covered = flatUnionCheck(acc, schema, context) === null ? void 0 : isExclusiveUnion(schema) ? "oneOf" : "anyOf";
276
+ const lines = [];
277
+ for (const keyword of ["anyOf", "oneOf"]) {
278
+ if (keyword === covered)
279
+ continue;
280
+ const members = record[keyword];
281
+ if (!Array.isArray(members) || members.length === 0)
282
+ continue;
283
+ const matches = [];
284
+ for (const member of members) {
285
+ const match = subschemaMatchExpr(acc, member, matchContext(context));
286
+ if (match === null)
287
+ break;
288
+ matches.push(match);
289
+ }
290
+ if (matches.length !== members.length)
291
+ continue;
292
+ const check = unionMatchExpr(keyword, matches);
293
+ if (check === null || check === "true")
294
+ continue;
295
+ const failure = throwError(`${label} does not match ${keyword === "anyOf" ? "any of" : "exactly one of"} the declared schemas`);
296
+ lines.push(optional ? ` if (${acc} !== undefined && !(${check})) ${failure};` : ` if (!(${check})) ${failure};`);
297
+ }
298
+ return lines;
299
+ };
300
+ const unionMatchExpr = (keyword, matches) => {
301
+ if (keyword === "anyOf") {
302
+ if (matches.includes("true"))
303
+ return null;
304
+ return matches.length === 1 ? matches[0] : `(${matches.join(" || ")})`;
305
+ }
306
+ if (matches.length === 1)
307
+ return matches[0];
308
+ return `((${matches.map((match) => `(${match} ? 1 : 0)`).join(" + ")}) === 1)`;
309
+ };
310
+ const generateConditionalChecks = (acc, schema, label, context) => {
311
+ if (!isSchemaObject(schema))
312
+ return [];
313
+ const record = schema;
314
+ if (!("if" in record) || !("then" in record) && !("else" in record))
315
+ return [];
316
+ const ifMatch = subschemaMatchExpr(acc, record["if"], matchContext(context));
317
+ if (ifMatch === null)
318
+ return [];
319
+ const lines = [];
320
+ if ("then" in record) {
321
+ const thenMatch = subschemaMatchExpr(acc, record["then"], matchContext(context));
322
+ if (thenMatch === null)
323
+ return [];
324
+ if (thenMatch !== "true") {
325
+ lines.push(` if ((${ifMatch}) && !(${thenMatch})) ${throwError(`${label} does not satisfy the 'then' schema`)};`);
326
+ }
327
+ }
328
+ if ("else" in record) {
329
+ const elseMatch = subschemaMatchExpr(acc, record["else"], matchContext(context));
330
+ if (elseMatch === null)
331
+ return [];
332
+ if (elseMatch !== "true") {
333
+ lines.push(` if (!(${ifMatch}) && !(${elseMatch})) ${throwError(`${label} does not satisfy the 'else' schema`)};`);
334
+ }
335
+ }
336
+ return lines;
101
337
  };
102
- const generateContainsCheck = (acc, schema, label) => {
338
+ const generateKeyedValueChecks = (obj, schema, label, context = {}) => {
339
+ if (!isSchemaObject(schema))
340
+ return [];
341
+ const record = schema;
342
+ const patternEntries = [];
343
+ const patternSource = record["patternProperties"];
344
+ if (typeof patternSource === "object" && patternSource !== null && !Array.isArray(patternSource)) {
345
+ for (const [pattern, sub] of Object.entries(patternSource)) {
346
+ const match = subschemaMatchExpr("_v", sub, matchContext(context));
347
+ if (match === null || match === "true")
348
+ continue;
349
+ patternEntries.push([pattern, match]);
350
+ }
351
+ }
352
+ const allPatterns = typeof patternSource === "object" && patternSource !== null && !Array.isArray(patternSource) ? Object.keys(patternSource) : [];
353
+ const additional = record["additionalProperties"];
354
+ const additionalMatch = additional !== void 0 && typeof additional !== "boolean" ? subschemaMatchExpr("_v", additional, matchContext(context)) : null;
355
+ const lines = [];
356
+ for (const [pattern, match] of patternEntries) {
357
+ lines.push(` if (${regexLiteral(pattern)}.test(_k) && !(${match})) ${throwError(`${label} invalid value for property `, "_k")};`);
358
+ }
359
+ if (additionalMatch !== null && additionalMatch !== "true") {
360
+ const declared = hasProperties(schema) ? Object.keys(schema.properties) : [];
361
+ const guards = [];
362
+ if (declared.length > 0)
363
+ guards.push(`!${JSON.stringify(declared)}.includes(_k)`);
364
+ for (const pattern of allPatterns)
365
+ guards.push(`!${regexLiteral(pattern)}.test(_k)`);
366
+ const prefix = guards.length > 0 ? `${guards.join(" && ")} && ` : "";
367
+ lines.push(` if (${prefix}!(${additionalMatch})) ${throwError(`${label} invalid value for property `, "_k")};`);
368
+ }
369
+ if (lines.length === 0)
370
+ return [];
371
+ return [
372
+ ` for (const _k of Object.keys(${obj})) {`,
373
+ ` const _v = (${obj} as Record<string, unknown>)[_k];`,
374
+ ...lines,
375
+ ` }`
376
+ ];
377
+ };
378
+ const generateContainsCheck = (acc, schema, label, context = {}) => {
103
379
  if (!isSchemaObject(schema))
104
380
  return [];
105
381
  const sp = schema;
106
382
  if (!("contains" in sp))
107
383
  return [];
108
- const match = subschemaMatchExpr("_c", sp["contains"]);
384
+ const match = subschemaMatchExpr("_c", sp["contains"], matchContext(context));
109
385
  if (match === null)
110
386
  return [];
111
387
  const min = typeof sp["minContains"] === "number" ? sp["minContains"] : 1;
@@ -126,12 +402,12 @@ const generateDependentRequiredChecks = (obj, schema, label) => {
126
402
  if (!Array.isArray(deps))
127
403
  continue;
128
404
  for (const dep of deps) {
129
- lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${JSON.stringify(dep)} in ${obj})) ${throwError(`${label} must have property '${dep}' when '${trigger}' is present`)};`);
405
+ lines.push(` if (${hasOwnCheck(obj, trigger)} && ${missingCheck(obj, dep)}) ${throwError(`${label} must have property '${dep}' when '${trigger}' is present`)};`);
130
406
  }
131
407
  }
132
408
  return lines;
133
409
  };
134
- const generateDependentSchemasChecks = (obj, schema, label) => {
410
+ const generateDependentSchemasChecks = (obj, schema, label, context) => {
135
411
  const dep = schema["dependentSchemas"];
136
412
  if (typeof dep !== "object" || dep === null || Array.isArray(dep))
137
413
  return [];
@@ -140,13 +416,13 @@ const generateDependentSchemasChecks = (obj, schema, label) => {
140
416
  if (sub === true)
141
417
  continue;
142
418
  if (sub === false) {
143
- lines.push(` if (${JSON.stringify(trigger)} in ${obj}) ${throwError(`${label} must NOT have property '${trigger}'`)};`);
419
+ lines.push(` if (${hasOwnCheck(obj, trigger)}) ${throwError(`${label} must NOT have property '${trigger}'`)};`);
144
420
  continue;
145
421
  }
146
- const match = subschemaMatchExpr(obj, sub);
422
+ const match = subschemaMatchExpr(obj, sub, matchContext(context));
147
423
  if (match === null || match === "true")
148
424
  continue;
149
- lines.push(` if (${JSON.stringify(trigger)} in ${obj} && !(${match})) ${throwError(`${label} does not satisfy the schema required when '${trigger}' is present`)};`);
425
+ lines.push(` if (${hasOwnCheck(obj, trigger)} && !(${match})) ${throwError(`${label} does not satisfy the schema required when '${trigger}' is present`)};`);
150
426
  }
151
427
  return lines;
152
428
  };
@@ -158,7 +434,8 @@ const resolvePositionSchema = (pos, rootSchema) => {
158
434
  }
159
435
  return pos;
160
436
  };
161
- const generatePrefixItemsAssertion = (acc, field, schema, rootSchema) => {
437
+ const generatePrefixItemsAssertion = (acc, field, schema, context) => {
438
+ const rootSchema = context.rootSchema;
162
439
  const prefix = getPrefixItems(schema);
163
440
  if (!prefix)
164
441
  return [];
@@ -168,9 +445,8 @@ const generatePrefixItemsAssertion = (acc, field, schema, rootSchema) => {
168
445
  const el = `${acc}[${i}]`;
169
446
  const present = `Array.isArray(${acc}) && ${acc}.length > ${i}`;
170
447
  if (isSchemaObject(pos) && hasEnum(pos) && pos.enum.length > 0) {
171
- const allowed = JSON.stringify(pos.enum);
172
448
  const label = pos.enum.map((v) => JSON.stringify(v)).join(", ");
173
- lines.push(` if (${present} && !(${allowed} as readonly unknown[]).includes(${el})) ${throwError(`${field}[${i}] must be one of: ${label}`)};`);
449
+ lines.push(` if (${present} && !${generateEnumCheck(el, pos.enum)}) ${throwError(`${field}[${i}] must be one of: ${label}`)};`);
174
450
  continue;
175
451
  }
176
452
  if (isSchemaObject(pos) && hasType(pos)) {
@@ -180,16 +456,20 @@ const generatePrefixItemsAssertion = (acc, field, schema, rootSchema) => {
180
456
  lines.push(` if (${present} && (${wrong})) ${throwError(`${field}[${i}] expected ${typeLabel(pt)}, got `, `typeof ${el}`)};`);
181
457
  }
182
458
  }
459
+ const match = subschemaMatchExpr(el, prefix[i], matchContext(context));
460
+ if (match !== null && match !== "true") {
461
+ lines.push(` if (${present} && !(${match})) ${throwError(`${field}[${i}] does not match the tuple schema`)};`);
462
+ }
183
463
  }
184
464
  if (prefixItemsCapsLength(schema)) {
185
465
  lines.push(` if (Array.isArray(${acc}) && ${acc}.length > ${prefix.length}) ${throwError(`${field} must NOT have more than ${prefix.length} items`)};`);
186
466
  }
187
467
  return lines;
188
468
  };
189
- const generatePropertyNameChecks = (obj, schema, label) => {
469
+ const generatePropertyNameChecks = (obj, schema, label, context) => {
190
470
  if (!hasPropertyNames(schema) || !isSchemaObject(schema.propertyNames))
191
471
  return [];
192
- const match = subschemaMatchExpr("_name", schema.propertyNames);
472
+ const match = subschemaMatchExpr("_name", schema.propertyNames, matchContext(context));
193
473
  if (match === null || match === "true")
194
474
  return [];
195
475
  return [
@@ -198,13 +478,19 @@ const generatePropertyNameChecks = (obj, schema, label) => {
198
478
  ` }`
199
479
  ];
200
480
  };
201
- const generateObjectKeywordChecks = (obj, schema, label, guard) => {
481
+ const generateCompositionChecks = (acc, schema, label, context = {}) => [
482
+ ...generateNotCheck(acc, schema, label, false, context),
483
+ ...generateAllOfChecks(acc, schema, label, false, context),
484
+ ...generateUnionMatchChecks(acc, schema, label, false, context),
485
+ ...generateConditionalChecks(acc, schema, label, context)
486
+ ];
487
+ const generateObjectKeywordChecks = (obj, schema, label, guard, context = {}) => {
202
488
  if (!isSchemaObject(schema))
203
489
  return [];
204
490
  const inner = [
205
491
  ...generateDependentRequiredChecks(obj, schema, label),
206
- ...generateDependentSchemasChecks(obj, schema, label),
207
- ...generatePropertyNameChecks(obj, schema, label)
492
+ ...generateDependentSchemasChecks(obj, schema, label, context),
493
+ ...generatePropertyNameChecks(obj, schema, label, context)
208
494
  ];
209
495
  if (inner.length === 0)
210
496
  return [];
@@ -217,16 +503,25 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
217
503
  const field = `[${typeName}] field '${key}'`;
218
504
  const lines = [];
219
505
  if (isRequired) {
220
- lines.push(` if (!(${JSON.stringify(key)} in input)) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
506
+ lines.push(` if (${missingCheck("input", key)}) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
507
+ }
508
+ if (propSchema === false) {
509
+ lines.push(` if (${hasOwnCheck("input", key)}) ${throwError(`[${typeName}] must NOT have property '${key}'`)};`);
510
+ return lines;
221
511
  }
222
512
  if (!isSchemaObject(propSchema))
223
513
  return lines;
514
+ lines.push(...generateBackstopAssertion(acc, propSchema, field, context, !isRequired));
224
515
  if (hasRef(propSchema))
225
516
  return lines;
517
+ lines.push(...generateNotCheck(acc, propSchema, field, !isRequired, context));
518
+ lines.push(...generateAllOfChecks(acc, propSchema, field, !isRequired, context));
519
+ lines.push(...generateUnionMatchChecks(acc, propSchema, field, !isRequired, context));
520
+ lines.push(...generateConditionalChecks(acc, propSchema, field, context));
226
521
  const unionBranches = getUnionBranches(propSchema);
227
522
  if (unionBranches) {
228
523
  if (!context.stripUnknown && canEnforceUnion(unionBranches, context.rootSchema)) {
229
- const check = generateUnionCheck(acc, unionBranches, context.useRefImports ?? false, context.suffix ?? "");
524
+ const check = generateUnionCheck(acc, unionBranches, context.useRefImports ?? false, context.suffix ?? "", isExclusiveUnion(propSchema));
230
525
  if (check !== null) {
231
526
  const failure = throwError(`${field} does not match any allowed variant`);
232
527
  lines.push(isRequired ? ` if (!(${check})) ${failure};` : ` if (${acc} !== undefined && !(${check})) ${failure};`);
@@ -252,13 +547,19 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
252
547
  }
253
548
  return lines;
254
549
  }
550
+ if (hasConst(propSchema)) {
551
+ const equal = generateDeepEqualCheck(acc, propSchema.const);
552
+ const failure = throwError(`${field} must be ${JSON.stringify(propSchema.const)}`);
553
+ lines.push(isRequired ? ` if (!(${equal})) ${failure};` : ` if (${acc} !== undefined && !(${equal})) ${failure};`);
554
+ return lines;
555
+ }
255
556
  if (hasEnum(propSchema)) {
256
- const allowed = JSON.stringify(propSchema.enum);
557
+ const member = generateEnumCheck(acc, propSchema.enum);
257
558
  const label = propSchema.enum.map((v) => JSON.stringify(v)).join(", ");
258
559
  if (isRequired) {
259
- lines.push(` if (!(${allowed} as readonly unknown[]).includes(${acc})) ${throwError(`${field} must be one of: ${label}`)};`);
560
+ lines.push(` if (!${member}) ${throwError(`${field} must be one of: ${label}`)};`);
260
561
  } else {
261
- lines.push(` if (${acc} !== undefined && !(${allowed} as readonly unknown[]).includes(${acc})) ${throwError(`${field} must be one of: ${label}`)};`);
562
+ lines.push(` if (${acc} !== undefined && !${member}) ${throwError(`${field} must be one of: ${label}`)};`);
262
563
  }
263
564
  return lines;
264
565
  }
@@ -272,6 +573,7 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
272
573
  const nonNull = types.filter((type) => type !== "null");
273
574
  if (nonNull.length === 1) {
274
575
  lines.push(...generateConstraintChecks(acc, { ...propSchema, type: nonNull[0] }, field, context));
576
+ lines.push(...nullableObjectShapeChecks(acc, propSchema, nonNull[0], field, !isRequired, context));
275
577
  }
276
578
  return lines;
277
579
  }
@@ -291,9 +593,15 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
291
593
  if (isSchemaObject(propSchema)) {
292
594
  const r = propSchema;
293
595
  if ("contains" in r)
294
- lines.push(...generateContainsCheck(acc, propSchema, field));
596
+ lines.push(...generateContainsCheck(acc, propSchema, field, context));
295
597
  if (("dependentRequired" in r || "dependentSchemas" in r || "propertyNames" in r) && !isInlineObjectProperty(propSchema)) {
296
- lines.push(...generateObjectKeywordChecks(acc, propSchema, field, true));
598
+ lines.push(...generateObjectKeywordChecks(acc, propSchema, field, true, context));
599
+ }
600
+ if ("patternProperties" in r || isSchemaObject(propSchema) && typeof r["additionalProperties"] === "object") {
601
+ const keyed = generateKeyedValueChecks(acc, propSchema, field, context);
602
+ if (keyed.length > 0) {
603
+ lines.push(` if (isObject(${acc})) {`, ...keyed.map((line) => ` ${line}`), ` }`);
604
+ }
297
605
  }
298
606
  }
299
607
  return lines;
@@ -327,18 +635,27 @@ const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
327
635
  required.delete(key);
328
636
  }
329
637
  for (const key of required) {
330
- lines.push(` if (!(${JSON.stringify(key)} in input)) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
638
+ lines.push(` if (${missingCheck("input", key)}) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
331
639
  }
332
640
  for (const member of inlineAllOfMembers(schema)) {
333
641
  lines.push(...generateObjectStrictAssertion(member, typeName, context).slice(1));
334
642
  }
335
643
  const sr = schema;
336
644
  if ("dependentRequired" in sr || "dependentSchemas" in sr || "propertyNames" in sr) {
337
- lines.push(...generateObjectKeywordChecks("input", schema, `[${typeName}]`, false));
645
+ lines.push(...generateObjectKeywordChecks("input", schema, `[${typeName}]`, false, context));
338
646
  }
647
+ lines.push(...generateObjectSizeChecks("input", schema, `[${typeName}]`, false));
648
+ lines.push(...generateNotCheck("input", schema, `[${typeName}]`, false, context));
649
+ lines.push(...generateAllOfChecks("input", schema, `[${typeName}]`, false, context, true));
650
+ lines.push(...generateUnionMatchChecks("input", schema, `[${typeName}]`, false, context));
651
+ lines.push(...generateConditionalChecks("input", schema, `[${typeName}]`, context));
652
+ lines.push(...generateKeyedValueChecks("input", schema, `[${typeName}]`, context));
653
+ lines.push(...generateBackstopAssertion("input", schema, `[${typeName}]`, context));
339
654
  return lines;
340
655
  };
341
- const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
656
+ const generateScalarStrictAssertion = (schema, typeName, context = {}) => {
657
+ if (schema === false)
658
+ return ` ${throwError(`[${typeName}] schema is false: no value is valid`)};`;
342
659
  const got = 'input === null ? "null" : typeof input';
343
660
  const instanceOf = getMjstInstanceOf(schema);
344
661
  if (instanceOf) {
@@ -353,13 +670,15 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
353
670
  const label = `[${typeName}]`;
354
671
  const lines = [];
355
672
  if (hasConst(schema)) {
356
- const wanted = JSON.stringify(JSON.stringify(schema.const));
357
- lines.push(` if (JSON.stringify(input) !== ${wanted}) ${throwError(`${label} must be ${JSON.stringify(schema.const)}`)};`);
673
+ lines.push(` if (!(${generateDeepEqualCheck("input", schema.const)})) ${throwError(`${label} must be ${JSON.stringify(schema.const)}`)};`);
358
674
  } else if (hasEnum(schema)) {
359
- const allowed = JSON.stringify(schema.enum);
360
675
  const enumLabel = schema.enum.map((v) => JSON.stringify(v)).join(", ");
361
- lines.push(` if (!(${allowed} as readonly unknown[]).includes(input)) ${throwError(`${label} must be one of: ${enumLabel}`)};`);
676
+ lines.push(` if (!${generateEnumCheck("input", schema.enum)}) ${throwError(`${label} must be one of: ${enumLabel}`)};`);
362
677
  }
678
+ lines.push(...generateNotCheck("input", schema, label, false, context));
679
+ lines.push(...generateAllOfChecks("input", schema, label, false, context));
680
+ lines.push(...generateUnionMatchChecks("input", schema, label, false, context));
681
+ lines.push(...generateConditionalChecks("input", schema, label, context));
363
682
  const rootMultiType = multiTypeCheck("input", schema, { ignoreConstraints: true });
364
683
  if (rootMultiType !== void 0) {
365
684
  const types = schema.type;
@@ -368,8 +687,10 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
368
687
  }
369
688
  const nonNull = types.filter((type) => type !== "null");
370
689
  if (nonNull.length === 1) {
371
- lines.push(...generateConstraintChecks("input", { ...schema, type: nonNull[0] }, label, rootSchema ? { rootSchema } : {}));
690
+ lines.push(...generateConstraintChecks("input", { ...schema, type: nonNull[0] }, label, context));
691
+ lines.push(...nullableObjectShapeChecks("input", schema, nonNull[0], label, false, context));
372
692
  }
693
+ lines.push(...generateBackstopAssertion("input", schema, label, context));
373
694
  return lines.length > 0 ? lines.join("\n") : null;
374
695
  }
375
696
  if (hasType(schema)) {
@@ -379,15 +700,22 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
379
700
  lines.push(` if (${wrongType}) ${throwError(`${label} expected ${typeLabel(t)}, got `, got)};`);
380
701
  }
381
702
  if (t === "array") {
382
- lines.push(...generateContainsCheck("input", schema, label));
703
+ lines.push(...generateContainsCheck("input", schema, label, context));
383
704
  }
384
- lines.push(...generateConstraintChecks("input", schema, label, rootSchema ? { rootSchema } : {}));
705
+ lines.push(...generateConstraintChecks("input", schema, label, context));
385
706
  }
707
+ lines.push(...generateBackstopAssertion("input", schema, label, context));
386
708
  return lines.length > 0 ? lines.join("\n") : null;
387
709
  };
388
710
  export {
711
+ backstopReason,
712
+ backstopSchema,
713
+ generateBackstopAssertion,
714
+ generateCompositionChecks,
389
715
  generateContainsCheck,
716
+ generateKeyedValueChecks,
390
717
  generateObjectKeywordChecks,
718
+ generateObjectSizeChecks,
391
719
  generateObjectStrictAssertion,
392
720
  generateScalarStrictAssertion,
393
721
  inlineAllOfMembers