@amritk/validation 0.3.1 → 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
|
-
|
|
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 ===
|
|
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++;
|
|
@@ -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
|
|
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
|
|
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 = ${
|
|
417
|
-
lines.push(` if (_ir !== true) ${
|
|
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) =>
|
|
1656
|
-
|
|
1657
|
-
|
|
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
|
-
|
|
1663
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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",
|