@amritk/generate-validators 0.11.1 → 0.11.2

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,7 +1,7 @@
1
1
  import { refToFilename } from '@amritk/helpers/ref-to-filename';
2
2
  import { refToName } from '@amritk/helpers/ref-to-name';
3
3
  import { resolveRef } from '@amritk/helpers/resolve-ref';
4
- import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasItems, hasOneOf, hasProperties, hasRef, } from '@amritk/helpers/schema-guards';
4
+ import { hasRef } from '@amritk/helpers/schema-guards';
5
5
  /**
6
6
  * Generates an import statement for a single $ref, importing both the type
7
7
  * and the validator function from the ref's generated file.
@@ -10,7 +10,9 @@ const buildImport = (ref, suffix) => {
10
10
  const filename = refToFilename(ref);
11
11
  const typeName = refToName(ref, suffix);
12
12
  const validatorName = `validate${typeName}`;
13
- return `import { type ${typeName}, ${validatorName} } from './${filename}'`;
13
+ // `.js` extension so the emitted import resolves under Node ESM (not just Bun);
14
+ // `./x.js` → sibling `x.ts` is the standard NodeNext form.
15
+ return `import { type ${typeName}, ${validatorName} } from './${filename}.js'`;
14
16
  };
15
17
  /**
16
18
  * Resolves the canonical filename for a ref, stripping `-or-reference` suffixes
@@ -21,46 +23,57 @@ const canonicalFilename = (ref) => {
21
23
  return refToFilename(base);
22
24
  };
23
25
  /**
24
- * Walks one level of the schema and yields all direct $ref strings that should
25
- * become imports: properties, additionalProperties, items, and union branches.
26
+ * Recursively walks a schema and yields every `$ref` the validator emitter can
27
+ * turn into a `validateX(...)` call, in traversal order. The emitter recurses
28
+ * into far more than properties/items/additionalProperties/top-level
29
+ * combinators: it also delegates for `patternProperties`, `propertyNames`,
30
+ * `if`/`then`/`else`, `contains`, `prefixItems`, `dependentSchemas`, `not`, and
31
+ * objects nested inside any combinator branch. A `$ref` reached by *any* of those
32
+ * paths must become an import, or the generated file references an undefined
33
+ * `validateX`. (Mirrors the parsers package's `collect-imports` traversal.)
26
34
  */
27
- const collectDirectRefs = (schema) => {
28
- if (typeof schema === 'boolean' || schema === null)
29
- return [];
30
- const refs = [];
35
+ const collectDirectRefs = (value, refs = []) => {
36
+ if (typeof value !== 'object' || value === null)
37
+ return refs;
38
+ if (Array.isArray(value)) {
39
+ for (const item of value)
40
+ collectDirectRefs(item, refs);
41
+ return refs;
42
+ }
43
+ const schema = value;
44
+ // A `$ref` is a leaf: the emitter delegates the whole value to the referenced
45
+ // validator, so record the ref and do not descend past it.
31
46
  if (hasRef(schema)) {
32
47
  refs.push(schema.$ref);
33
48
  return refs;
34
49
  }
35
- const propSchemas = 'properties' in schema && typeof schema.properties === 'object' && schema.properties !== null
36
- ? Object.values(schema.properties)
37
- : [];
38
- for (const prop of propSchemas) {
39
- if (hasRef(prop))
40
- refs.push(prop.$ref);
41
- if (hasItems(prop) && hasRef(prop.items))
42
- refs.push(prop.items.$ref);
43
- if (hasAdditionalProperties(prop) && hasRef(prop.additionalProperties)) {
44
- refs.push(prop.additionalProperties.$ref);
50
+ // Every keyword whose subschema(s) the emitter recurses into. `properties` and
51
+ // `patternProperties` hold subschemas as object *values*; the combinator/tuple
52
+ // keywords hold them in arrays; the rest are single subschemas. We deliberately
53
+ // do NOT descend into `$defs`/`definitions` — those are split into their own
54
+ // generated files, not inlined by this validator. `collectDirectRefs`
55
+ // self-guards on non-objects, so a keyword that is a boolean or missing is a
56
+ // harmless no-op.
57
+ const subSchemaMaps = ['properties', 'patternProperties'];
58
+ for (const mapKey of subSchemaMaps) {
59
+ const map = schema[mapKey];
60
+ if (typeof map === 'object' && map !== null && !Array.isArray(map)) {
61
+ for (const sub of Object.values(map))
62
+ collectDirectRefs(sub, refs);
45
63
  }
46
- // Inline nested objects are validated recursively by the generator, so any
47
- // $refs anywhere inside them must become imports as well.
48
- if (hasProperties(prop))
49
- refs.push(...collectDirectRefs(prop));
50
64
  }
51
- if (hasItems(schema) && hasRef(schema.items)) {
52
- refs.push(schema.items.$ref);
65
+ const singleSubSchemas = ['items', 'additionalProperties', 'propertyNames', 'contains', 'if', 'then', 'else', 'not'];
66
+ for (const key of singleSubSchemas) {
67
+ if (key in schema)
68
+ collectDirectRefs(schema[key], refs);
53
69
  }
54
- if (hasAdditionalProperties(schema) && hasRef(schema.additionalProperties)) {
55
- refs.push(schema.additionalProperties.$ref);
56
- }
57
- for (const branch of [
58
- ...(hasOneOf(schema) ? schema.oneOf : []),
59
- ...(hasAnyOf(schema) ? schema.anyOf : []),
60
- ...(hasAllOf(schema) ? schema.allOf : []),
61
- ]) {
62
- if (hasRef(branch))
63
- refs.push(branch.$ref);
70
+ const arraySubSchemas = ['oneOf', 'anyOf', 'allOf', 'prefixItems'];
71
+ for (const key of arraySubSchemas) {
72
+ const arr = schema[key];
73
+ if (Array.isArray(arr)) {
74
+ for (const sub of arr)
75
+ collectDirectRefs(sub, refs);
76
+ }
64
77
  }
65
78
  return refs;
66
79
  };
@@ -35,12 +35,13 @@ export const generateValidatorFile = (schema, typeName, options) => {
35
35
  const typeDefinition = generateTypeDefinition(schema, typeName, { typeSuffix });
36
36
  const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix);
37
37
  const booleanGuard = generateBooleanGuard(schema, typeName, typeSuffix);
38
- let result = `import type { ValidationResult, ValidationError } from './validation-result'\n`;
38
+ // `.js` extension so the relative import resolves under Node ESM, not only Bun.
39
+ let result = `import type { ValidationResult, ValidationError } from './validation-result.js'\n`;
39
40
  // `const` checks on object/array values call the runtime `valuesEqual` helper.
40
41
  // Only import it when the generated body actually uses it, so files without a
41
42
  // structural `const` do not carry an unused import.
42
43
  if (validatorFunction.includes('valuesEqual(')) {
43
- result += `import { valuesEqual } from './validation-result'\n`;
44
+ result += `import { valuesEqual } from './validation-result.js'\n`;
44
45
  }
45
46
  for (const imp of refImports) {
46
47
  result += imp + '\n';
@@ -1,5 +1,6 @@
1
1
  import { escapeRegexPattern } from '@amritk/helpers/escape-regex-pattern';
2
2
  import { getMjstInstanceOf, getMjstPrimitive } from '@amritk/helpers/mjst-extension';
3
+ import { multipleOfFailExpr, multipleOfPassExpr } from '@amritk/helpers/multiple-of-check';
3
4
  import { refToName } from '@amritk/helpers/ref-to-name';
4
5
  import { safeAccessor } from '@amritk/helpers/safe-accessor';
5
6
  import { hasAdditionalProperties, hasAllOf, hasAnyOf, hasConst, hasDependentRequired, hasEnum, hasExclusiveMaximum, hasExclusiveMinimum, hasItems, hasMaxItems, hasMaximum, hasMaxLength, hasMinItems, hasMinimum, hasMinLength, hasMultipleOf, hasOneOf, hasPattern, hasProperties, hasPropertyNames, hasRef, hasRequired, hasStrictExclusiveMaximum, hasStrictExclusiveMinimum, hasType, hasUniqueItems, isObjectSchema, isSchemaObject, } from '@amritk/helpers/schema-guards';
@@ -53,6 +54,19 @@ const wrongTypeCondition = (accessor, type) => {
53
54
  return '';
54
55
  }
55
56
  };
57
+ /**
58
+ * Returns the list of type names when a schema's `type` is an array (the JSON
59
+ * Schema multi-type / nullable idiom, e.g. `["string","null"]`), else `null`.
60
+ * `hasType` only recognises a *string* `type`, so without special handling a
61
+ * multi-type schema slips through every branch and emits NO check — not even a
62
+ * required-presence check. A multi-type is validated as the *disjunction* of its
63
+ * per-type checks (the value must match at least one).
64
+ */
65
+ const getTypeArray = (schema) => {
66
+ if (!isSchemaObject(schema) || !('type' in schema) || !Array.isArray(schema.type))
67
+ return null;
68
+ return schema.type;
69
+ };
56
70
  const createRootContext = () => ({ objVar: 'obj', pathPrefix: '${_path}', depth: 0, hoisted: [] });
57
71
  /**
58
72
  * Returns the `patternProperties` regex sources, or an empty array when the
@@ -147,7 +161,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
147
161
  const vName = validatorName(refToName(ref, suffix));
148
162
  if (isRequired) {
149
163
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
150
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
164
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
151
165
  lines.push(` } else {`);
152
166
  lines.push(` const _r = ${vName}(${raw}, ${path})`);
153
167
  lines.push(` if (_r !== true) errors.push(..._r.errors)`);
@@ -166,7 +180,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
166
180
  if (instanceOf) {
167
181
  if (isRequired) {
168
182
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
169
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
183
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
170
184
  lines.push(` } else if (!(${raw} instanceof ${instanceOf})) {`);
171
185
  lines.push(` errors.push({ message: 'must be ${instanceOf}', path: ${path} })`);
172
186
  lines.push(` }`);
@@ -183,7 +197,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
183
197
  if (primitive) {
184
198
  if (isRequired) {
185
199
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
186
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
200
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
187
201
  lines.push(` } else if (typeof ${raw} !== "${primitive}") {`);
188
202
  lines.push(` errors.push({ message: 'must be ${primitive}', path: ${path} })`);
189
203
  lines.push(` }`);
@@ -201,7 +215,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
201
215
  const msg = JSON.stringify(`must be ${JSON.stringify(propSchema.const)}`);
202
216
  if (isRequired) {
203
217
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
204
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
218
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
205
219
  lines.push(` } else if (${mismatch}) {`);
206
220
  lines.push(` errors.push({ message: ${msg}, path: ${path} })`);
207
221
  lines.push(` }`);
@@ -219,16 +233,49 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
219
233
  const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
220
234
  if (isRequired) {
221
235
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
222
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
236
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
223
237
  lines.push(` } else if (!(${allowed} as unknown[]).includes(${raw})) {`);
224
- lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`);
238
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
225
239
  lines.push(` }`);
226
240
  }
227
241
  else {
228
242
  lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
229
- lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`);
243
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
244
+ lines.push(` }`);
245
+ }
246
+ return lines;
247
+ }
248
+ // Multi-type / nullable property (array `type`, e.g. `["string","null"]`).
249
+ // `hasType` is false for an array `type`, so without this the property emits no
250
+ // check at all. The value is valid when it matches ANY listed type, i.e. an
251
+ // error is reported only when it is the wrong type for EVERY listed type; the
252
+ // required-presence check is still emitted so a missing required prop fails.
253
+ const typeArray = getTypeArray(propSchema);
254
+ if (typeArray) {
255
+ const allWrong = typeArray
256
+ .map((t) => wrongTypeCondition(raw, t))
257
+ .filter((c) => c !== '')
258
+ .map((c) => `(${c})`)
259
+ .join(' && ');
260
+ const label = typeArray.map((t) => typeofString(t)).join(' or ');
261
+ if (isRequired) {
262
+ lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
263
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
264
+ if (allWrong) {
265
+ lines.push(` } else if (${allWrong}) {`);
266
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
267
+ }
268
+ lines.push(` }`);
269
+ }
270
+ else if (allWrong) {
271
+ lines.push(` if (${raw} !== undefined && (${allWrong})) {`);
272
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be ${label}`)}, path: ${path} })`);
230
273
  lines.push(` }`);
231
274
  }
275
+ // Any sibling value constraints (e.g. `minLength` on a `["string","null"]`)
276
+ // still apply — each carries its own runtime-type guard, so it is a no-op for
277
+ // the values it does not target.
278
+ lines.push(...generateConstraintChecks(key, raw, path, propSchema, suffix, ctx));
232
279
  return lines;
233
280
  }
234
281
  // typed property
@@ -238,7 +285,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
238
285
  const typLabel = typeofString(t);
239
286
  if (isRequired) {
240
287
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
241
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
288
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
242
289
  if (wrongType) {
243
290
  lines.push(` } else if (${wrongType}) {`);
244
291
  lines.push(` errors.push({ message: 'must be ${typLabel}', path: ${path} })`);
@@ -267,7 +314,7 @@ const generatePropertyChecks = (key, propSchema, isRequired, suffix, ctx) => {
267
314
  if (!hasType(propSchema) && isRequired) {
268
315
  // No `type` to anchor a missing-property check, so enforce presence here.
269
316
  lines.push(` if (!(${JSON.stringify(key)} in ${ctx.objVar})) {`);
270
- lines.push(` errors.push({ message: "must have required property '${key}'", path: ${parentPath} })`);
317
+ lines.push(` errors.push({ message: ${JSON.stringify(`must have required property '${key}'`)}, path: ${parentPath} })`);
271
318
  lines.push(` } else {`);
272
319
  lines.push(...extraLines);
273
320
  lines.push(` }`);
@@ -353,7 +400,7 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
353
400
  lines.push(` }`);
354
401
  }
355
402
  if (hasMultipleOf(propSchema)) {
356
- lines.push(` if (typeof ${raw} === 'number' && ${raw} % ${propSchema.multipleOf} !== 0) {`);
403
+ lines.push(` if (typeof ${raw} === 'number' && ${multipleOfFailExpr(raw, propSchema.multipleOf)}) {`);
357
404
  lines.push(` errors.push({ message: 'must be a multiple of ${propSchema.multipleOf}', path: ${path} })`);
358
405
  lines.push(` }`);
359
406
  }
@@ -496,7 +543,7 @@ const generateValueChecks = (key, raw, path, propSchema, suffix, ctx) => {
496
543
  const allowed = JSON.stringify(propSchema.enum);
497
544
  const label = propSchema.enum.map((v) => JSON.stringify(v)).join(', ');
498
545
  lines.push(` if (${raw} !== undefined && !(${allowed} as unknown[]).includes(${raw})) {`);
499
- lines.push(` errors.push({ message: \`must be one of: ${label}\`, path: ${path} })`);
546
+ lines.push(` errors.push({ message: ${JSON.stringify(`must be one of: ${label}`)}, path: ${path} })`);
500
547
  lines.push(` }`);
501
548
  return lines;
502
549
  }
@@ -1098,7 +1145,7 @@ const booleanLeafExpr = (schema, acc) => {
1098
1145
  if (hasExclusiveMaximum(schema))
1099
1146
  parts.push(`!(${acc} >= ${schema.exclusiveMaximum})`);
1100
1147
  if (hasMultipleOf(schema))
1101
- parts.push(`${acc} % ${schema.multipleOf} === 0`);
1148
+ parts.push(multipleOfPassExpr(acc, schema.multipleOf));
1102
1149
  return parts.join(' && ');
1103
1150
  }
1104
1151
  case 'boolean':
@@ -1305,7 +1352,7 @@ const generateScalarValidator = (schema, typeName, suffix) => {
1305
1352
  return [
1306
1353
  `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1307
1354
  ` if (!(${allowed} as unknown[]).includes(input)) {`,
1308
- ` return { valid: false, errors: [{ message: \`must be one of: ${label}\`, path: _path }] }`,
1355
+ ` return { valid: false, errors: [{ message: ${JSON.stringify(`must be one of: ${label}`)}, path: _path }] }`,
1309
1356
  ` }`,
1310
1357
  ` return true`,
1311
1358
  `}`,
@@ -1327,31 +1374,48 @@ const generateScalarValidator = (schema, typeName, suffix) => {
1327
1374
  `}`,
1328
1375
  ].join('\n');
1329
1376
  }
1377
+ // Top-level multi-type / nullable schema (array `type`, e.g. `["string","null"]`).
1378
+ // `hasType` is false for an array `type`, so without this a root multi-type
1379
+ // schema falls through to the final `return true` and validates NOTHING. The
1380
+ // value is valid when it matches any listed type.
1381
+ const rootTypeArray = getTypeArray(schema);
1382
+ if (rootTypeArray) {
1383
+ const allWrong = rootTypeArray
1384
+ .map((t) => wrongTypeCondition('input', t))
1385
+ .filter((c) => c !== '')
1386
+ .map((c) => `(${c})`)
1387
+ .join(' && ');
1388
+ const label = rootTypeArray.map((t) => typeofString(t)).join(' or ');
1389
+ if (!allWrong) {
1390
+ return [
1391
+ `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
1392
+ ` return true`,
1393
+ `}`,
1394
+ ].join('\n');
1395
+ }
1396
+ return [
1397
+ `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1398
+ ` if (${allWrong}) {`,
1399
+ ` return { valid: false, errors: [{ message: ${JSON.stringify(`must be ${label}`)}, path: _path }] }`,
1400
+ ` }`,
1401
+ ` return true`,
1402
+ `}`,
1403
+ ].join('\n');
1404
+ }
1330
1405
  // Top-level typed schema (string, number, boolean, array)
1331
1406
  if (hasType(schema)) {
1332
1407
  const t = schema.type;
1333
1408
  const wrongType = wrongTypeCondition('input', t);
1334
1409
  const typLabel = typeofString(t);
1335
- const constraintLines = [];
1336
- if (t === 'string') {
1337
- if (hasPattern(schema)) {
1338
- const re = escapeRegexPattern(schema.pattern);
1339
- const msg = JSON.stringify(`must match pattern ${schema.pattern}`);
1340
- constraintLines.push(` if (typeof input === 'string' && !/${re}/.test(input)) {`);
1341
- constraintLines.push(` errors.push({ message: ${msg}, path: _path })`);
1342
- constraintLines.push(` }`);
1343
- }
1344
- if (hasMinLength(schema)) {
1345
- constraintLines.push(` if (typeof input === 'string' && input.length < ${schema.minLength}) {`);
1346
- constraintLines.push(` errors.push({ message: 'must have at least ${schema.minLength} characters', path: _path })`);
1347
- constraintLines.push(` }`);
1348
- }
1349
- if (hasMaxLength(schema)) {
1350
- constraintLines.push(` if (typeof input === 'string' && input.length > ${schema.maxLength}) {`);
1351
- constraintLines.push(` errors.push({ message: 'must have at most ${schema.maxLength} characters', path: _path })`);
1352
- constraintLines.push(` }`);
1353
- }
1354
- }
1410
+ // Reuse the shared constraint emitter — the per-property path already handles
1411
+ // string (pattern, min/maxLength), number/integer (bounds, multipleOf) and
1412
+ // array (items, min/maxItems, uniqueItems, contains, prefixItems). The root
1413
+ // path previously only built string constraints, so a `{type:'number',
1414
+ // minimum:5}` or `{type:'array', minItems:2}` root accepted invalid input.
1415
+ // `raw` is `input`; `path` is the root `_path` (as a template so the shared
1416
+ // emitter's `path.slice(1,-1)` for array-item indices still works).
1417
+ const rootCtx = createRootContext();
1418
+ const constraintLines = generateConstraintChecks('', 'input', '`${_path}`', schema, suffix, rootCtx);
1355
1419
  if (!wrongType) {
1356
1420
  return [
1357
1421
  `export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`,
@@ -1369,8 +1433,11 @@ const generateScalarValidator = (schema, typeName, suffix) => {
1369
1433
  `}`,
1370
1434
  ].join('\n');
1371
1435
  }
1436
+ // Array-item / nested constraints can hoist module-level declarations (e.g. a
1437
+ // compiled known-keys set); emit them before the function so it references them.
1438
+ const hoistedBlock = rootCtx.hoisted.length > 0 ? `${rootCtx.hoisted.join('\n')}\n\n` : '';
1372
1439
  return [
1373
- `export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1440
+ `${hoistedBlock}export const ${vName} = (input: unknown, _path = ''): ValidationResult => {`,
1374
1441
  ` if (${wrongType}) {`,
1375
1442
  ` return { valid: false, errors: [{ message: 'must be ${typLabel}', path: _path }] }`,
1376
1443
  ` }`,
@@ -1382,6 +1449,44 @@ const generateScalarValidator = (schema, typeName, suffix) => {
1382
1449
  }
1383
1450
  return [`export const ${vName} = (_input: unknown, _path = ''): ValidationResult => {`, ` return true`, `}`].join('\n');
1384
1451
  };
1452
+ /**
1453
+ * Throws when a schema (anywhere in its subtree) uses a keyword this generator
1454
+ * does not implement but which *narrows* the set of valid documents. Today that
1455
+ * is `unevaluatedProperties` / `unevaluatedItems` with a constraining value
1456
+ * (`false` or a subschema). The generator has no support for them — only the
1457
+ * runtime interpreter does — so silently emitting a validator would produce one
1458
+ * that ACCEPTS documents the interpreter REJECTS: a wrong verdict, worse than an
1459
+ * error. `unevaluated*: true` is a no-op (it permits everything), so it is
1460
+ * allowed through.
1461
+ *
1462
+ * We deliberately throw rather than implement the keywords: doing them correctly
1463
+ * requires tracking which properties/items each combinator branch "evaluated",
1464
+ * which is a large, separate feature. Failing loudly at generation time surfaces
1465
+ * the gap instead of shipping a validator that lies.
1466
+ */
1467
+ const assertNoUnsupportedKeywords = (schema, typeName) => {
1468
+ const visit = (node) => {
1469
+ if (typeof node !== 'object' || node === null)
1470
+ return;
1471
+ if (Array.isArray(node)) {
1472
+ for (const item of node)
1473
+ visit(item);
1474
+ return;
1475
+ }
1476
+ const record = node;
1477
+ for (const keyword of ['unevaluatedProperties', 'unevaluatedItems']) {
1478
+ // `true` permits everything → no constraint → safe to ignore.
1479
+ if (keyword in record && record[keyword] !== true) {
1480
+ throw new Error(`[${typeName}] unsupported keyword "${keyword}": the validator generator does not implement it and would ` +
1481
+ `silently accept documents the interpreter rejects. Validate this schema with the runtime interpreter, ` +
1482
+ `or remove the keyword.`);
1483
+ }
1484
+ }
1485
+ for (const value of Object.values(record))
1486
+ visit(value);
1487
+ };
1488
+ visit(schema);
1489
+ };
1385
1490
  /**
1386
1491
  * Generates a TypeScript validator function from a JSON Schema.
1387
1492
  *
@@ -1405,6 +1510,7 @@ const generateScalarValidator = (schema, typeName, suffix) => {
1405
1510
  * ```
1406
1511
  */
1407
1512
  export const generateValidatorFunction = (schema, typeName, suffix = '') => {
1513
+ assertNoUnsupportedKeywords(schema, typeName);
1408
1514
  if (isObjectSchema(schema)) {
1409
1515
  return generateObjectValidator(schema, typeName, suffix);
1410
1516
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/generate-validators",
3
- "version": "0.11.1",
3
+ "version": "0.11.2",
4
4
  "description": "Generate TypeScript validation functions from JSON Schemas.",
5
5
  "module": "./dist/index.js",
6
6
  "type": "module",
@@ -46,7 +46,7 @@
46
46
  },
47
47
  "dependencies": {
48
48
  "json-schema-typed": "^8.0.1",
49
- "@amritk/helpers": "0.10.1"
49
+ "@amritk/helpers": "0.10.2"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@ryoppippi/unplugin-typia": "^2.6.5",