@amritk/validation 0.3.1 → 0.4.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.
@@ -101,18 +101,20 @@ const getTypeArray = (schema) => {
101
101
  const ROOT_ERROR_SINK = "(errors ??= [])";
102
102
  const ACCUMULATING = { on: false, blocked: false };
103
103
  const FAIL_FAST_SINK = "\0fail-fast";
104
+ const MATCH_BOOL_SINK = "\0match-bool";
105
+ const returnsOnReport = (sink) => sink === FAIL_FAST_SINK || sink === MATCH_BOOL_SINK;
104
106
  const LITERAL_CONDITION = /^[\s()!&|]*(?:(?:true|false)[\s()!&|]*)+$/;
105
107
  const blockFailFast = (ctx) => {
106
- if (ctx.sink === FAIL_FAST_SINK)
108
+ if (returnsOnReport(ctx.sink))
107
109
  ctx.failFast.blocked = true;
108
110
  };
109
- const hoistPrefix = (ctx) => ctx.failFast.on ? "_check" : "";
110
- const typeTestAccessor = (raw, ctx) => ctx.sink === FAIL_FAST_SINK ? `(${raw} as unknown)` : raw;
111
+ const hoistPrefix = (ctx) => ctx.hoistNamespace;
112
+ const typeTestAccessor = (raw, ctx) => returnsOnReport(ctx.sink) ? `(${raw} as unknown)` : raw;
111
113
  const emittedName = (typeName, failFast) => failFast.on ? checkerName(typeName) : validatorName(typeName);
112
114
  const errorsBinding = (failFast) => failFast.on ? [] : [" let errors: ValidationError[] | undefined"];
113
115
  const errorsVerdict = (failFast) => failFast.on ? " return true" : " return errors !== undefined ? { valid: false, errors } : true";
114
116
  const returnError = (message, path, keyword, params = "{}") => `return { valid: false, errors: [{ message: ${message}, path: ${path}, keyword: ${JSON.stringify(keyword)}, params: ${params} }] }`;
115
- const pushError = (sink, message, path, keyword, params = "{}") => sink === FAIL_FAST_SINK ? returnError(message, path, keyword, params) : `${sink}.push({ message: ${message}, path: ${path}, keyword: ${JSON.stringify(keyword)}, params: ${params} })`;
117
+ const pushError = (sink, message, path, keyword, params = "{}") => sink === FAIL_FAST_SINK ? returnError(message, path, keyword, params) : sink === MATCH_BOOL_SINK ? "return false" : `${sink}.push({ message: ${message}, path: ${path}, keyword: ${JSON.stringify(keyword)}, params: ${params} })`;
116
118
  const MATCH_ERROR_SINK = "_m";
117
119
  const createRootContext = (rootSchema, formats = NO_FORMATS, branchErrors = false, failFast = ACCUMULATING) => ({
118
120
  objVar: "obj",
@@ -127,7 +129,10 @@ const createRootContext = (rootSchema, formats = NO_FORMATS, branchErrors = fals
127
129
  // `selectBranchErrors` sweep *after* the `return` that ends the function, which
128
130
  // is unreachable code the consumer's build rejects.
129
131
  branchErrors: branchErrors && !failFast.on,
130
- failFast
132
+ failFast,
133
+ hoistNamespace: failFast.on ? "_check" : "",
134
+ refGuard: guardName,
135
+ namedMatchers: /* @__PURE__ */ new Map()
131
136
  });
132
137
  const pointerSegment = (key) => {
133
138
  const pointer = key.replace(/~/g, "~0").replace(/\//g, "~1");
@@ -143,8 +148,22 @@ const patternPropertySources = (schema) => {
143
148
  };
144
149
  const readsBinding = (name, text) => new RegExp(`\\b${name}\\b`).test(text);
145
150
  const readsObjBinding = (text) => readsBinding("obj", text);
151
+ const keptHoisted = (hoisted, text) => {
152
+ const kept = /* @__PURE__ */ new Set();
153
+ const scan = (source) => {
154
+ for (const entry of hoisted) {
155
+ if (kept.has(entry) || !source.includes(entry.reference))
156
+ continue;
157
+ kept.add(entry);
158
+ if (entry.calls === true)
159
+ scan(entry.declaration);
160
+ }
161
+ };
162
+ scan(text);
163
+ return hoisted.filter((entry) => kept.has(entry)).map((entry) => entry.declaration);
164
+ };
146
165
  const withHoisted = (hoisted, text) => {
147
- const kept = hoisted.filter((entry) => text.includes(entry.reference)).map((entry) => entry.declaration);
166
+ const kept = keptHoisted(hoisted, text);
148
167
  const firstBreak = text.indexOf("\n");
149
168
  const signature = firstBreak === -1 ? text : text.slice(0, firstBreak);
150
169
  const rest = firstBreak === -1 ? "" : text.slice(firstBreak);
@@ -251,7 +270,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
251
270
  const lines = [];
252
271
  if (hasRef(schema)) {
253
272
  const target = refToName(schema.$ref, suffix);
254
- const delegate = ctx.sink === FAIL_FAST_SINK ? [` const _r = ${checkerName(target)}(${raw}, ${path})`, ` if (_r !== true) return _r`] : [
273
+ const delegate = ctx.sink === FAIL_FAST_SINK ? [` const _r = ${checkerName(target)}(${raw}, ${path})`, ` if (_r !== true) return _r`] : ctx.sink === MATCH_BOOL_SINK ? [` if (!${ctx.refGuard(target)}(${raw})) return false`] : [
255
274
  ` const _r = ${validatorName(target)}(${raw}, ${path})`,
256
275
  ` if (_r !== true) ${ctx.sink}.push(..._r.errors)`
257
276
  ];
@@ -411,10 +430,15 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
411
430
  if (hasRef(itemSchema) && !declaresKeywordOutside(itemSchema, ["$ref"])) {
412
431
  const failFast = ctx.sink === FAIL_FAST_SINK;
413
432
  const target = refToName(itemSchema.$ref, suffix);
433
+ const onFailure = failFast ? "return _ir" : `${ctx.sink}.push(..._ir.errors)`;
414
434
  lines.push(` if (Array.isArray(${raw})) {`);
415
435
  lines.push(` for (let ${iv} = ${firstTailIndex}; ${iv} < ${raw}.length; ${iv}++) {`);
416
- lines.push(` const _ir = ${failFast ? checkerName(target) : validatorName(target)}(${raw}[${iv}], ${itemPath})`);
417
- lines.push(` if (_ir !== true) ${failFast ? "return _ir" : `${ctx.sink}.push(..._ir.errors)`}`);
436
+ if (ctx.sink === MATCH_BOOL_SINK) {
437
+ lines.push(` if (!${ctx.refGuard(target)}(${raw}[${iv}])) return false`);
438
+ } else {
439
+ lines.push(` const _ir = ${emittedName(target, ctx.failFast)}(${raw}[${iv}], ${itemPath})`);
440
+ lines.push(` if (_ir !== true) ${onFailure}`);
441
+ }
418
442
  lines.push(` }`);
419
443
  lines.push(` }`);
420
444
  } else if (isSchemaObject(itemSchema)) {
@@ -541,12 +565,50 @@ const generateValueCheckLines = (_key, raw, path, propSchema, suffix, ctx, requi
541
565
  hoisted: ctx.hoisted,
542
566
  rootSchema: ctx.rootSchema,
543
567
  sink: ctx.sink,
544
- formats: ctx.formats
568
+ formats: ctx.formats,
569
+ hoistNamespace: ctx.hoistNamespace,
570
+ refGuard: ctx.refGuard,
571
+ namedMatchers: ctx.namedMatchers
545
572
  };
546
573
  lines.push(...generateKeywordChecks("", raw, path, propSchema, suffix, valueCtx, presence));
547
574
  return lines;
548
575
  };
549
576
  const NARROWABLE_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\[[A-Za-z_$][\w$]*\]|\[\d+\])*$/;
577
+ const namedMatcher = (sub, suffix, ctx) => {
578
+ const key = JSON.stringify(sub);
579
+ const existing = ctx.namedMatchers.get(key);
580
+ if (existing !== void 0)
581
+ return existing;
582
+ const state = { on: false, blocked: false };
583
+ const fnCtx = {
584
+ ...ctx,
585
+ objVar: "obj",
586
+ pathPrefix: "${_path}",
587
+ depth: 0,
588
+ sink: MATCH_BOOL_SINK,
589
+ failFast: state,
590
+ branchErrors: false
591
+ };
592
+ const checks = generateValueChecks("", "input", "`${_path}`", sub, suffix, fnCtx, true);
593
+ if (checks.length === 0)
594
+ return "true";
595
+ if (state.blocked)
596
+ return null;
597
+ const body = checks.join("\n");
598
+ const name = `${hoistPrefix(ctx)}_match${ctx.namedMatchers.size}`;
599
+ const declaration = [
600
+ `const ${name} = (${readsBinding("input", body) ? "input" : "_input"}: unknown): boolean => {`,
601
+ // Some checks build a path for a key or an index before they know whether
602
+ // they fail. Nothing reports it here, so it is rooted at nothing.
603
+ ...readsBinding("_path", body) ? [` const _path = ''`] : [],
604
+ body,
605
+ ` return true`,
606
+ `}`
607
+ ].join("\n");
608
+ ctx.namedMatchers.set(key, name);
609
+ ctx.hoisted.push({ declaration, reference: `${name}(`, calls: true });
610
+ return name;
611
+ };
550
612
  const generateMatchesExpr = (raw, sub, suffix, ctx, required = false, branch) => {
551
613
  if (sub === true)
552
614
  return "true";
@@ -556,12 +618,26 @@ const generateMatchesExpr = (raw, sub, suffix, ctx, required = false, branch) =>
556
618
  return "true";
557
619
  const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
558
620
  const valuePath = branch === void 0 ? "`${_path}`" : branch.path;
621
+ const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
622
+ `;
623
+ if (branch === void 0) {
624
+ const named = namedMatcher(sub, suffix, ctx);
625
+ if (named !== null)
626
+ return named === "true" ? "true" : `${named}(${raw})`;
627
+ const matchState = { on: ctx.failFast.on, blocked: false };
628
+ const matchCtx = { ...ctx, sink: MATCH_BOOL_SINK, failFast: matchState, branchErrors: false };
629
+ const boolChecks = generateValueChecks("", value, valuePath, sub, suffix, matchCtx, required);
630
+ if (boolChecks.length === 0)
631
+ return "true";
632
+ if (!matchState.blocked)
633
+ return `((): boolean => {
634
+ ${binding}${boolChecks.join("\n")}
635
+ return true })()`;
636
+ }
559
637
  const checks = generateValueChecks("", value, valuePath, sub, suffix, { ...ctx, sink: MATCH_ERROR_SINK }, required);
560
638
  if (checks.length === 0)
561
639
  return "true";
562
640
  const body = checks.join("\n");
563
- const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
564
- `;
565
641
  const collect = branch === void 0 ? "" : `
566
642
  if (_m.length !== 0) (${branch.buffer} ??= []).push(_m)`;
567
643
  return `((): boolean => { const _m: ValidationError[] = []
@@ -768,7 +844,10 @@ const generateInlineObjectChecks = (key, propSchema, raw, suffix, ctx) => {
768
844
  hoisted: ctx.hoisted,
769
845
  rootSchema: ctx.rootSchema,
770
846
  sink: ctx.sink,
771
- formats: ctx.formats
847
+ formats: ctx.formats,
848
+ hoistNamespace: ctx.hoistNamespace,
849
+ refGuard: ctx.refGuard,
850
+ namedMatchers: ctx.namedMatchers
772
851
  };
773
852
  const required = new Set(hasRequired(propSchema) ? propSchema.required : []);
774
853
  const properties = hasProperties(propSchema) ? propSchema.properties : {};
@@ -806,7 +885,10 @@ const generatePropertyNameChecks = (nameSchema, suffix, ctx) => {
806
885
  hoisted: ctx.hoisted,
807
886
  rootSchema: ctx.rootSchema,
808
887
  sink: ctx.sink,
809
- formats: ctx.formats
888
+ formats: ctx.formats,
889
+ hoistNamespace: ctx.hoistNamespace,
890
+ refGuard: ctx.refGuard,
891
+ namedMatchers: ctx.namedMatchers
810
892
  };
811
893
  const checks = generateValueChecks("", "_name", at, nameSchema, suffix, nameCtx, true);
812
894
  if (checks.length === 0)
@@ -1372,7 +1454,8 @@ const typeDescribesEveryAcceptedValue = (schema) => {
1372
1454
  }
1373
1455
  return !IMPLICIT_OBJECT_KEYWORDS.some((keyword) => declaresKey(s, keyword));
1374
1456
  };
1375
- const generateBooleanGuard = (schema, typeName, _suffix = "", unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => {
1457
+ const generateBooleanGuard = (schema, typeName, _suffix = "", unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => generateBooleanGuardSource(schema, typeName, unknownKeys, formats).code;
1458
+ const generateBooleanGuardSource = (schema, typeName, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => {
1376
1459
  const name = guardName(typeName);
1377
1460
  const returns = typeDescribesEveryAcceptedValue(rewriteNullable(schema)) ? `input is ${typeName}` : "boolean";
1378
1461
  const fallback = `export const ${name} = (input: unknown): ${returns} => ${validatorName(typeName)}(input) === true`;
@@ -1380,12 +1463,12 @@ const generateBooleanGuard = (schema, typeName, _suffix = "", unknownKeys = DEFA
1380
1463
  const ctx = createGuardContext(unknownKeys, formats);
1381
1464
  if (declaresObjectType(rewritten)) {
1382
1465
  if (!objectRootIsSelfContained(rewritten))
1383
- return fallback;
1466
+ return { code: fallback, standalone: false };
1384
1467
  const block = booleanObjectParts(rewritten, "input", "obj", ctx);
1385
1468
  if (block === null)
1386
- return fallback;
1469
+ return { code: fallback, standalone: false };
1387
1470
  const body = isFlatGuardBlock(block) ? [` return (`, block.conditions.map((part) => ` ${part}`).join(" &&\n"), ` )`] : [...renderGuardBlock(block, "return false", " ", "lines", unknownKeys), ` return true`];
1388
- return [
1471
+ const code = [
1389
1472
  `export const ${name} = (input: unknown): ${returns} => {`,
1390
1473
  // Same unused-local as the validator's hot guard: a node with no property
1391
1474
  // to read guards on the shape alone and never touches the narrowing.
@@ -1393,11 +1476,12 @@ const generateBooleanGuard = (schema, typeName, _suffix = "", unknownKeys = DEFA
1393
1476
  ...body,
1394
1477
  `}`
1395
1478
  ].join("\n");
1479
+ return { code, standalone: true };
1396
1480
  }
1397
1481
  const expr = booleanLeafExpr(rewritten, "input", ctx);
1398
1482
  if (expr === null)
1399
- return fallback;
1400
- return `export const ${name} = (input: unknown): ${returns} => ${expr}`;
1483
+ return { code: fallback, standalone: false };
1484
+ return { code: `export const ${name} = (input: unknown): ${returns} => ${expr}`, standalone: true };
1401
1485
  };
1402
1486
  const generateScalarValidator = (schema, typeName, suffix, rootSchema, formats, branchErrors, failFast) => {
1403
1487
  const vName = emittedName(typeName, failFast);
@@ -1652,16 +1736,24 @@ const rewriteNullable = (node) => {
1652
1736
  return { anyOf: [{ type: "null" }, out] };
1653
1737
  return out;
1654
1738
  };
1655
- const generateValidatorFunction = (schema, typeName, suffix = "", rootSchema, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS, branchErrors = false) => generateValidatorSource(schema, typeName, suffix, rootSchema, unknownKeys, formats, branchErrors, {
1656
- on: false,
1657
- blocked: false
1658
- });
1739
+ const generateValidatorFunction = (schema, typeName, suffix = "", rootSchema, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS, branchErrors = false) => {
1740
+ const source = generateValidatorSource(schema, typeName, suffix, rootSchema, unknownKeys, formats, branchErrors, {
1741
+ on: false,
1742
+ blocked: false
1743
+ });
1744
+ assertNoSinkLeaked(source, MATCH_BOOL_SINK, typeName, "validator");
1745
+ return source;
1746
+ };
1747
+ const assertNoSinkLeaked = (source, sink, typeName, what) => {
1748
+ if (!source.includes(sink))
1749
+ return;
1750
+ throw new Error(`the ${what} for "${typeName}" emitted the error sink verbatim, which means some emitter reported through \`ctx.sink\` without going through \`pushError\`. That report has to take the short-circuiting form (a \`return\`) or stay on the accumulating path.`);
1751
+ };
1659
1752
  const generateCheckFunction = (schema, typeName, suffix = "", rootSchema, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => {
1660
1753
  const failFast = { on: true, blocked: false };
1661
1754
  const source = generateValidatorSource(schema, typeName, suffix, rootSchema, unknownKeys, formats, false, failFast);
1662
- if (source.includes(FAIL_FAST_SINK)) {
1663
- throw new Error(`the fail-fast validator for "${typeName}" emitted the error sink verbatim, which means some emitter reported through \`ctx.sink\` without going through \`pushError\`. That report has to take the fail-fast form (a \`return\`) or stay on the accumulating path.`);
1664
- }
1755
+ assertNoSinkLeaked(source, FAIL_FAST_SINK, typeName, "fail-fast validator");
1756
+ assertNoSinkLeaked(source, MATCH_BOOL_SINK, typeName, "fail-fast validator");
1665
1757
  if (!failFast.blocked)
1666
1758
  return source;
1667
1759
  return [
@@ -1671,6 +1763,31 @@ const generateCheckFunction = (schema, typeName, suffix = "", rootSchema, unknow
1671
1763
  `}`
1672
1764
  ].join("\n");
1673
1765
  };
1766
+ const createSubschemaMatcher = (suffix, rootSchema, formats, hoistNamespace, refGuard = guardName) => {
1767
+ const ctx = { ...createRootContext(rootSchema, formats), hoistNamespace, refGuard };
1768
+ return {
1769
+ test: (sub) => {
1770
+ if (sub === false)
1771
+ return false;
1772
+ const rewritten = rewriteNullable(sub);
1773
+ const state = { on: false, blocked: false };
1774
+ const checks = generateValueChecks("", "input", "`${_path}`", rewritten, suffix, { ...ctx, sink: MATCH_BOOL_SINK, failFast: state, branchErrors: false }, true);
1775
+ if (checks.length === 0)
1776
+ return true;
1777
+ if (!state.blocked)
1778
+ return `${checks.join("\n")}
1779
+ return true`;
1780
+ return ` return ${generateMatchesExpr("input", rewritten, suffix, ctx, true)}`;
1781
+ },
1782
+ named: (sub) => {
1783
+ if (sub === false)
1784
+ return false;
1785
+ const name = namedMatcher(rewriteNullable(sub), suffix, ctx);
1786
+ return name === "true" ? true : name;
1787
+ },
1788
+ declarations: (text) => keptHoisted(ctx.hoisted, text)
1789
+ };
1790
+ };
1674
1791
  const generateValidatorSource = (schema, typeName, suffix, rootSchema, unknownKeys, formats, branchErrors, failFast) => {
1675
1792
  assertGeneratableRefs(schema, typeName);
1676
1793
  const rewritten = rewriteNullable(schema);
@@ -1685,7 +1802,9 @@ const generateValidatorSource = (schema, typeName, suffix, rootSchema, unknownKe
1685
1802
  return generateScalarValidator(rewritten, typeName, suffix, document, formats, branchErrors, failFast);
1686
1803
  };
1687
1804
  export {
1805
+ createSubschemaMatcher,
1688
1806
  generateBooleanGuard,
1807
+ generateBooleanGuardSource,
1689
1808
  generateCheckFunction,
1690
1809
  generateValidatorFunction
1691
1810
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/validation",
3
- "version": "0.3.1",
3
+ "version": "0.4.0",
4
4
  "description": "One generator surface over mjst's types, validators, coercers, repairers and parsers.",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -73,11 +73,11 @@
73
73
  }
74
74
  },
75
75
  "dependencies": {
76
- "@amritk/helpers": "^0.23.2",
76
+ "@amritk/helpers": "^0.24.0",
77
77
  "json-schema-typed": "^8.0.1"
78
78
  },
79
79
  "devDependencies": {
80
- "@amritk/runtime-validators": "^0.15.0",
80
+ "@amritk/runtime-validators": "^0.15.1",
81
81
  "@ryoppippi/unplugin-typia": "^2.6.5",
82
82
  "@scalar/openapi-parser": "^0.26.1",
83
83
  "@sinclair/typebox": "^0.34.49",