@amritk/generate-parsers 0.17.2 → 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.
Files changed (32) hide show
  1. package/AI.md +10 -0
  2. package/README.md +147 -19
  3. package/dist/generators/assert-supported-keywords.d.ts +13 -9
  4. package/dist/generators/assert-supported-keywords.js +58 -18
  5. package/dist/generators/build-schema.d.ts +9 -1
  6. package/dist/generators/build-schema.js +5 -2
  7. package/dist/generators/generate-deep-equal-check.d.ts +24 -0
  8. package/dist/generators/generate-deep-equal-check.js +35 -0
  9. package/dist/generators/generate-discriminated-union-validation.js +1 -2
  10. package/dist/generators/generate-enum-check.d.ts +8 -3
  11. package/dist/generators/generate-enum-check.js +8 -6
  12. package/dist/generators/generate-files.d.ts +7 -0
  13. package/dist/generators/generate-files.js +23 -4
  14. package/dist/generators/generate-parser-function.d.ts +1 -1
  15. package/dist/generators/generate-parser-function.js +212 -65
  16. package/dist/generators/generate-schema-checks.js +10 -8
  17. package/dist/generators/generate-strict-assertion.d.ts +89 -5
  18. package/dist/generators/generate-strict-assertion.js +429 -58
  19. package/dist/generators/generate-type-checks.d.ts +46 -10
  20. package/dist/generators/generate-type-checks.js +112 -16
  21. package/dist/generators/generate-unique-items-check.d.ts +16 -0
  22. package/dist/generators/generate-unique-items-check.js +32 -0
  23. package/dist/generators/generate-validation-expression.d.ts +29 -4
  24. package/dist/generators/generate-validation-expression.js +15 -6
  25. package/dist/generators/subschema-match.d.ts +23 -4
  26. package/dist/generators/subschema-match.js +312 -48
  27. package/dist/generators/unevaluated-match.d.ts +19 -0
  28. package/dist/generators/unevaluated-match.js +206 -0
  29. package/dist/helpers/collect-imports.d.ts +8 -0
  30. package/dist/helpers/collect-imports.js +1 -1
  31. package/dist/helpers/get-default-value.js +45 -2
  32. package/package.json +9 -3
@@ -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 } 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));
86
191
  }
87
192
  return lines;
88
193
  };
89
- const generateItemCheck = (schema) => {
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`)};`);
204
+ }
205
+ return lines;
206
+ };
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))
99
220
  return null;
100
- return { check: scalarCheck, message: `items expected ${typeLabel(items.type)}` };
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;
101
262
  };
102
- const generateContainsCheck = (acc, schema, label) => {
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))
268
+ return null;
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;
337
+ };
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,33 @@ 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}`)};`);
563
+ }
564
+ return lines;
565
+ }
566
+ const multiType = multiTypeCheck(acc, propSchema, { ignoreConstraints: true });
567
+ if (multiType !== void 0) {
568
+ const types = propSchema.type;
569
+ if (multiType !== null) {
570
+ const expected = throwError(`${field} expected ${types.map(typeLabel).join(" | ")}, got `, `typeof ${acc}`);
571
+ lines.push(isRequired ? ` if (!${multiType}) ${expected};` : ` if (${acc} !== undefined && !${multiType}) ${expected};`);
572
+ }
573
+ const nonNull = types.filter((type) => type !== "null");
574
+ if (nonNull.length === 1) {
575
+ lines.push(...generateConstraintChecks(acc, { ...propSchema, type: nonNull[0] }, field, context));
576
+ lines.push(...nullableObjectShapeChecks(acc, propSchema, nonNull[0], field, !isRequired, context));
262
577
  }
263
578
  return lines;
264
579
  }
@@ -278,32 +593,69 @@ const generatePropertyAssertion = (key, propSchema, isRequired, typeName, contex
278
593
  if (isSchemaObject(propSchema)) {
279
594
  const r = propSchema;
280
595
  if ("contains" in r)
281
- lines.push(...generateContainsCheck(acc, propSchema, field));
596
+ lines.push(...generateContainsCheck(acc, propSchema, field, context));
282
597
  if (("dependentRequired" in r || "dependentSchemas" in r || "propertyNames" in r) && !isInlineObjectProperty(propSchema)) {
283
- 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
+ }
284
605
  }
285
606
  }
286
607
  return lines;
287
608
  };
609
+ const inlineAllOfMembers = (schema) => {
610
+ if (!isSchemaObject(schema) || !Array.isArray(schema.allOf))
611
+ return [];
612
+ return schema.allOf.filter((member) => {
613
+ if (!isSchemaObject(member) || hasRef(member))
614
+ return false;
615
+ const record = member;
616
+ if ("allOf" in record || "oneOf" in record || "anyOf" in record || "not" in record)
617
+ return false;
618
+ if ("if" in record || "then" in record || "else" in record)
619
+ return false;
620
+ return hasProperties(member) || hasRequired(member);
621
+ });
622
+ };
288
623
  const generateObjectStrictAssertion = (schema, typeName, context = {}) => {
289
624
  const lines = [];
290
625
  lines.push(` if (!isObject(input)) ${throwError(`[${typeName}] expected object, got `, 'input === null ? "null" : typeof input')};`);
291
626
  if (!isSchemaObject(schema))
292
627
  return lines;
628
+ const required = new Set(hasRequired(schema) ? schema.required : []);
293
629
  if (hasProperties(schema)) {
294
- const required = new Set(hasRequired(schema) ? schema.required : []);
295
630
  const props = schema.properties;
296
631
  for (const key in props) {
297
632
  lines.push(...generatePropertyAssertion(key, props[key], required.has(key), typeName, context));
298
633
  }
634
+ for (const key in props)
635
+ required.delete(key);
636
+ }
637
+ for (const key of required) {
638
+ lines.push(` if (${missingCheck("input", key)}) ${throwError(`[${typeName}] missing required property '${key}'`)};`);
639
+ }
640
+ for (const member of inlineAllOfMembers(schema)) {
641
+ lines.push(...generateObjectStrictAssertion(member, typeName, context).slice(1));
299
642
  }
300
643
  const sr = schema;
301
644
  if ("dependentRequired" in sr || "dependentSchemas" in sr || "propertyNames" in sr) {
302
- lines.push(...generateObjectKeywordChecks("input", schema, `[${typeName}]`, false));
645
+ lines.push(...generateObjectKeywordChecks("input", schema, `[${typeName}]`, false, context));
303
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));
304
654
  return lines;
305
655
  };
306
- 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`)};`;
307
659
  const got = 'input === null ? "null" : typeof input';
308
660
  const instanceOf = getMjstInstanceOf(schema);
309
661
  if (instanceOf) {
@@ -318,12 +670,28 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
318
670
  const label = `[${typeName}]`;
319
671
  const lines = [];
320
672
  if (hasConst(schema)) {
321
- const wanted = JSON.stringify(JSON.stringify(schema.const));
322
- 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)}`)};`);
323
674
  } else if (hasEnum(schema)) {
324
- const allowed = JSON.stringify(schema.enum);
325
675
  const enumLabel = schema.enum.map((v) => JSON.stringify(v)).join(", ");
326
- 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}`)};`);
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));
682
+ const rootMultiType = multiTypeCheck("input", schema, { ignoreConstraints: true });
683
+ if (rootMultiType !== void 0) {
684
+ const types = schema.type;
685
+ if (rootMultiType !== null) {
686
+ lines.push(` if (!${rootMultiType}) ${throwError(`${label} expected ${types.map(typeLabel).join(" | ")}, got `, got)};`);
687
+ }
688
+ const nonNull = types.filter((type) => type !== "null");
689
+ if (nonNull.length === 1) {
690
+ lines.push(...generateConstraintChecks("input", { ...schema, type: nonNull[0] }, label, context));
691
+ lines.push(...nullableObjectShapeChecks("input", schema, nonNull[0], label, false, context));
692
+ }
693
+ lines.push(...generateBackstopAssertion("input", schema, label, context));
694
+ return lines.length > 0 ? lines.join("\n") : null;
327
695
  }
328
696
  if (hasType(schema)) {
329
697
  const t = schema.type;
@@ -332,20 +700,23 @@ const generateScalarStrictAssertion = (schema, typeName, rootSchema) => {
332
700
  lines.push(` if (${wrongType}) ${throwError(`${label} expected ${typeLabel(t)}, got `, got)};`);
333
701
  }
334
702
  if (t === "array") {
335
- const itemCheck = generateItemCheck(schema);
336
- if (itemCheck) {
337
- lines.push(` if (!(input as readonly unknown[]).every((_it) => ${itemCheck.check})) ${throwError(`${label} ${itemCheck.message}`)};`);
338
- }
339
- lines.push(...generateContainsCheck("input", schema, label));
340
- lines.push(...generatePrefixItemsAssertion("input", label, schema, rootSchema));
703
+ lines.push(...generateContainsCheck("input", schema, label, context));
341
704
  }
342
- lines.push(...generateConstraintChecks("input", schema, label, rootSchema ? { rootSchema } : {}));
705
+ lines.push(...generateConstraintChecks("input", schema, label, context));
343
706
  }
707
+ lines.push(...generateBackstopAssertion("input", schema, label, context));
344
708
  return lines.length > 0 ? lines.join("\n") : null;
345
709
  };
346
710
  export {
711
+ backstopReason,
712
+ backstopSchema,
713
+ generateBackstopAssertion,
714
+ generateCompositionChecks,
347
715
  generateContainsCheck,
716
+ generateKeyedValueChecks,
348
717
  generateObjectKeywordChecks,
718
+ generateObjectSizeChecks,
349
719
  generateObjectStrictAssertion,
350
- generateScalarStrictAssertion
720
+ generateScalarStrictAssertion,
721
+ inlineAllOfMembers
351
722
  };