@amritk/validation 0.3.0 → 0.3.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.
@@ -12,7 +12,7 @@ import { DEFAULT_UNKNOWN_KEYS } from "@amritk/helpers/unknown-keys-strategy";
12
12
  import { findDiscriminator } from "../helpers/find-discriminator.js";
13
13
  import { getDiscriminatorValue } from "../helpers/get-discriminator-value.js";
14
14
  import { assertNoUnsupportedKeywords } from "./assert-supported-keywords.js";
15
- import { generateEnumCaseInsensitiveCoercion } from "./generate-enum-check.js";
15
+ import { generateEnumCaseInsensitiveCoercion, generateEnumCheck } from "./generate-enum-check.js";
16
16
  import { generateBackstopAssertion, generateCompositionChecks, generateContainsCheck, generateKeyedValueChecks, generateObjectKeywordChecks, generateObjectSizeChecks, generateObjectStrictAssertion, generateScalarStrictAssertion, inlineAllOfMembers } from "./generate-strict-assertion.js";
17
17
  import { canEnforceUnion, generatePropertyTypeCheck, generateUnionCheck, getUnionBranches, hasFastPathBlockingKeyword, isExclusiveUnion, isInlineObjectArrayProperty, isInlineObjectProperty, isInlineUnionProperty, isUnionArrayProperty, shapeValidatorName } from "./generate-type-checks.js";
18
18
  import { generateUniqueItemsCheck } from "./generate-unique-items-check.js";
@@ -472,11 +472,11 @@ ${assertions}
472
472
  return isPrimitive ? `export const ${functionName} = (input: unknown): ${typeName} => input === ${literal} ? input as ${typeName} : ${literal} as ${typeName};` : `export const ${functionName} = (input: unknown): ${typeName} => ${literal} as ${typeName};`;
473
473
  }
474
474
  if (hasEnum(schema) && schema.enum.length > 0) {
475
- const values = JSON.stringify(schema.enum);
476
475
  const fallback = JSON.stringify(schema.enum[0]);
477
476
  const ci = unionCtx?.caseInsensitive ? generateEnumCaseInsensitiveCoercion("input", schema.enum, fallback) : null;
478
477
  const coerced = ci ? `(${ci})` : fallback;
479
- return `export const ${functionName} = (input: unknown): ${typeName} => ${values}.includes(input as never) ? input as ${typeName} : ${coerced} as ${typeName};`;
478
+ const member = generateEnumCheck("input", schema.enum);
479
+ return `export const ${functionName} = (input: unknown): ${typeName} => ${member} ? input as ${typeName} : ${coerced} as ${typeName};`;
480
480
  }
481
481
  if (hasOneOf(schema) || hasAnyOf(schema)) {
482
482
  const branches = hasOneOf(schema) ? schema.oneOf : hasAnyOf(schema) ? schema.anyOf : [];
@@ -57,6 +57,23 @@ const collectImportTargets = (schema, options) => {
57
57
  }
58
58
  typeOnlyDepth--;
59
59
  };
60
+ const scoredBranchRefs = /* @__PURE__ */ new Set();
61
+ const collectScoredBranchRefs = (branch) => {
62
+ if (rootSchema === void 0 || !isSchemaObject(branch))
63
+ return;
64
+ const ref = readKey(branch, "$ref");
65
+ if (typeof ref !== "string" || !ref.startsWith("#") || scoredBranchRefs.has(ref))
66
+ return;
67
+ scoredBranchRefs.add(ref);
68
+ const target = resolveRef(ref, rootSchema);
69
+ if (!isSchemaObject(target))
70
+ return;
71
+ const properties = readKey(target, "properties");
72
+ if (typeof properties !== "object" || properties === null)
73
+ return;
74
+ for (const value of Object.values(properties))
75
+ collectRefsFromValue(value);
76
+ };
60
77
  const collectRefsFromValue = (value) => {
61
78
  if (typeof value !== "object" || value === null) {
62
79
  return;
@@ -108,11 +125,13 @@ const collectImportTargets = (schema, options) => {
108
125
  if (hasOneOf(record)) {
109
126
  for (const item of record.oneOf) {
110
127
  collectRefsFromValue(item);
128
+ collectScoredBranchRefs(item);
111
129
  }
112
130
  }
113
131
  if (hasAnyOf(record)) {
114
132
  for (const item of record.anyOf) {
115
133
  collectRefsFromValue(item);
134
+ collectScoredBranchRefs(item);
116
135
  }
117
136
  }
118
137
  if (hasAllOf(record)) {
@@ -184,11 +203,13 @@ const collectImportTargets = (schema, options) => {
184
203
  if (typeof schema === "object" && schema !== null && hasOneOf(schema)) {
185
204
  for (const item of schema.oneOf) {
186
205
  collectRefsFromValue(item);
206
+ collectScoredBranchRefs(item);
187
207
  }
188
208
  }
189
209
  if (typeof schema === "object" && schema !== null && hasAnyOf(schema)) {
190
210
  for (const item of schema.anyOf) {
191
211
  collectRefsFromValue(item);
212
+ collectScoredBranchRefs(item);
192
213
  }
193
214
  }
194
215
  if (typeof schema === "object" && schema !== null && hasAllOf(schema)) {
@@ -11,7 +11,17 @@ const declarationEnd = (source, start) => {
11
11
  quote = null;
12
12
  continue;
13
13
  }
14
- if (char === '"' || char === "'" || char === "`")
14
+ if (char === "/" && source[i + 1] === "*") {
15
+ const close = source.indexOf("*/", i + 2);
16
+ if (close === -1)
17
+ return -1;
18
+ i = close + 1;
19
+ } else if (char === "/" && source[i + 1] === "/") {
20
+ const newline = source.indexOf("\n", i + 2);
21
+ if (newline === -1)
22
+ return -1;
23
+ i = newline;
24
+ } else if (char === '"' || char === "'" || char === "`")
15
25
  quote = char;
16
26
  else if (char === "{" || char === "[" || char === "(")
17
27
  depth++;
@@ -1,4 +1,4 @@
1
- import { MJST_EXTENSION_KEY } from "@amritk/helpers/mjst-extension";
1
+ import { hasMjstHint, MJST_EXTENSION_KEY } from "@amritk/helpers/mjst-extension";
2
2
  import { isSchemaObject } from "@amritk/helpers/schema-guards";
3
3
  const ENFORCED_KEYWORDS = /* @__PURE__ */ new Set([
4
4
  "$ref",
@@ -50,6 +50,8 @@ const declaresKeywordOutside = (schema, owned, formats = NO_FORMATS) => {
50
50
  for (const keyword of Object.keys(record)) {
51
51
  if (owned.includes(keyword))
52
52
  continue;
53
+ if (keyword === MJST_EXTENSION_KEY && !hasMjstHint(record))
54
+ continue;
53
55
  if (ENFORCED_KEYWORDS.has(keyword))
54
56
  return true;
55
57
  if (keyword === "format" && enforcesFormat(record, formats))
@@ -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
111
  const hoistPrefix = (ctx) => ctx.failFast.on ? "_check" : "";
110
- const typeTestAccessor = (raw, ctx) => ctx.sink === FAIL_FAST_SINK ? `(${raw} as unknown)` : raw;
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",
@@ -251,7 +253,7 @@ const generateKeywordChecks = (key, raw, path, schema, suffix, ctx, presence) =>
251
253
  const lines = [];
252
254
  if (hasRef(schema)) {
253
255
  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`] : [
256
+ const delegate = ctx.sink === FAIL_FAST_SINK ? [` const _r = ${checkerName(target)}(${raw}, ${path})`, ` if (_r !== true) return _r`] : ctx.sink === MATCH_BOOL_SINK ? [` const _r = ${emittedName(target, ctx.failFast)}(${raw}, ${path})`, ` if (_r !== true) return false`] : [
255
257
  ` const _r = ${validatorName(target)}(${raw}, ${path})`,
256
258
  ` if (_r !== true) ${ctx.sink}.push(..._r.errors)`
257
259
  ];
@@ -411,10 +413,11 @@ const generateConstraintChecks = (key, raw, path, propSchema, suffix, ctx) => {
411
413
  if (hasRef(itemSchema) && !declaresKeywordOutside(itemSchema, ["$ref"])) {
412
414
  const failFast = ctx.sink === FAIL_FAST_SINK;
413
415
  const target = refToName(itemSchema.$ref, suffix);
416
+ const onFailure = failFast ? "return _ir" : ctx.sink === MATCH_BOOL_SINK ? "return false" : `${ctx.sink}.push(..._ir.errors)`;
414
417
  lines.push(` if (Array.isArray(${raw})) {`);
415
418
  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)`}`);
419
+ lines.push(` const _ir = ${emittedName(target, ctx.failFast)}(${raw}[${iv}], ${itemPath})`);
420
+ lines.push(` if (_ir !== true) ${onFailure}`);
418
421
  lines.push(` }`);
419
422
  lines.push(` }`);
420
423
  } else if (isSchemaObject(itemSchema)) {
@@ -556,12 +559,23 @@ const generateMatchesExpr = (raw, sub, suffix, ctx, required = false, branch) =>
556
559
  return "true";
557
560
  const value = NARROWABLE_REFERENCE.test(raw) ? raw : `_mv${ctx.depth}`;
558
561
  const valuePath = branch === void 0 ? "`${_path}`" : branch.path;
562
+ const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
563
+ `;
564
+ if (branch === void 0) {
565
+ const matchState = { on: ctx.failFast.on, blocked: false };
566
+ const matchCtx = { ...ctx, sink: MATCH_BOOL_SINK, failFast: matchState, branchErrors: false };
567
+ const boolChecks = generateValueChecks("", value, valuePath, sub, suffix, matchCtx, required);
568
+ if (boolChecks.length === 0)
569
+ return "true";
570
+ if (!matchState.blocked)
571
+ return `((): boolean => {
572
+ ${binding}${boolChecks.join("\n")}
573
+ return true })()`;
574
+ }
559
575
  const checks = generateValueChecks("", value, valuePath, sub, suffix, { ...ctx, sink: MATCH_ERROR_SINK }, required);
560
576
  if (checks.length === 0)
561
577
  return "true";
562
578
  const body = checks.join("\n");
563
- const binding = value === raw ? "" : `const ${value}: unknown = ${raw}
564
- `;
565
579
  const collect = branch === void 0 ? "" : `
566
580
  if (_m.length !== 0) (${branch.buffer} ??= []).push(_m)`;
567
581
  return `((): boolean => { const _m: ValidationError[] = []
@@ -1652,16 +1666,24 @@ const rewriteNullable = (node) => {
1652
1666
  return { anyOf: [{ type: "null" }, out] };
1653
1667
  return out;
1654
1668
  };
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
- });
1669
+ const generateValidatorFunction = (schema, typeName, suffix = "", rootSchema, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS, branchErrors = false) => {
1670
+ const source = generateValidatorSource(schema, typeName, suffix, rootSchema, unknownKeys, formats, branchErrors, {
1671
+ on: false,
1672
+ blocked: false
1673
+ });
1674
+ assertNoSinkLeaked(source, MATCH_BOOL_SINK, typeName, "validator");
1675
+ return source;
1676
+ };
1677
+ const assertNoSinkLeaked = (source, sink, typeName, what) => {
1678
+ if (!source.includes(sink))
1679
+ return;
1680
+ 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.`);
1681
+ };
1659
1682
  const generateCheckFunction = (schema, typeName, suffix = "", rootSchema, unknownKeys = DEFAULT_UNKNOWN_KEYS, formats = NO_FORMATS) => {
1660
1683
  const failFast = { on: true, blocked: false };
1661
1684
  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
- }
1685
+ assertNoSinkLeaked(source, FAIL_FAST_SINK, typeName, "fail-fast validator");
1686
+ assertNoSinkLeaked(source, MATCH_BOOL_SINK, typeName, "fail-fast validator");
1665
1687
  if (!failFast.blocked)
1666
1688
  return source;
1667
1689
  return [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@amritk/validation",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
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.1",
76
+ "@amritk/helpers": "^0.23.3",
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",