@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.
- package/AI.md +16 -0
- package/README.md +83 -1
- package/dist/parsers/generators/build-schema.js +12 -2
- package/dist/parsers/generators/generate-exact-half.d.ts +50 -0
- package/dist/parsers/generators/generate-exact-half.js +165 -0
- package/dist/parsers/generators/generate-files.d.ts +8 -0
- package/dist/parsers/generators/generate-files.js +11 -1
- package/dist/parsers/generators/generate-parser-function.d.ts +6 -0
- package/dist/parsers/generators/generate-parser-function.js +51 -13
- package/dist/parsers/helpers/collect-helpers.d.ts +1 -1
- package/dist/parsers/helpers/collect-helpers.js +25 -2
- package/dist/parsers/helpers/collect-imports.js +21 -0
- package/dist/rehome-parser-file.js +11 -1
- package/dist/validators/generators/build-schema.d.ts +1 -1
- package/dist/validators/generators/build-schema.js +46 -12
- package/dist/validators/generators/collect-validator-imports.d.ts +7 -0
- package/dist/validators/generators/collect-validator-imports.js +4 -1
- package/dist/validators/generators/generate-coerce-function.d.ts +46 -1
- package/dist/validators/generators/generate-coerce-function.js +181 -53
- package/dist/validators/generators/generate-files.js +9 -4
- package/dist/validators/generators/generate-validator-function.d.ts +48 -0
- package/dist/validators/generators/generate-validator-function.js +146 -27
- package/package.json +3 -3
|
@@ -2,7 +2,19 @@ import { regexFlagsFor } from "@amritk/helpers/escape-regex-pattern";
|
|
|
2
2
|
import { declaresKey, readKey } from "@amritk/helpers/read-key";
|
|
3
3
|
import { refToName } from "@amritk/helpers/ref-to-name";
|
|
4
4
|
import { isSchemaObject } from "@amritk/helpers/schema-guards";
|
|
5
|
+
import { NO_FORMATS } from "./enforced-keywords.js";
|
|
6
|
+
import { createSubschemaMatcher } from "./generate-validator-function.js";
|
|
5
7
|
const COERCIBLE_TYPES = /* @__PURE__ */ new Set(["string", "number", "integer", "boolean", "null"]);
|
|
8
|
+
const ANNOTATION_KEYWORDS = /* @__PURE__ */ new Set([
|
|
9
|
+
"title",
|
|
10
|
+
"description",
|
|
11
|
+
"$comment",
|
|
12
|
+
"examples",
|
|
13
|
+
"default",
|
|
14
|
+
"deprecated",
|
|
15
|
+
"readOnly",
|
|
16
|
+
"writeOnly"
|
|
17
|
+
]);
|
|
6
18
|
const coercerNameFor = (ref, typeSuffix) => `coerce${refToName(ref, typeSuffix)}Value`;
|
|
7
19
|
const declaresObjectKeys = (schema) => declaresKey(schema, "properties") || declaresKey(schema, "patternProperties") || declaresKey(schema, "additionalProperties");
|
|
8
20
|
const declaresArrayItems = (schema) => declaresKey(schema, "items") || declaresKey(schema, "prefixItems");
|
|
@@ -14,34 +26,121 @@ const soleScalarType = (schema) => {
|
|
|
14
26
|
const type = readKey(schema, "type");
|
|
15
27
|
return typeof type === "string" && COERCIBLE_TYPES.has(type) ? type : null;
|
|
16
28
|
};
|
|
17
|
-
const
|
|
29
|
+
const typeArrayScalars = (schema) => {
|
|
18
30
|
const declared = readKey(schema, "type");
|
|
19
|
-
if (Array.isArray(declared))
|
|
20
|
-
const types2 = declared.filter((entry) => typeof entry === "string");
|
|
21
|
-
return types2.length > 0 && types2.every((type) => COERCIBLE_TYPES.has(type)) ? types2 : null;
|
|
22
|
-
}
|
|
23
|
-
if (declared !== void 0)
|
|
31
|
+
if (!Array.isArray(declared))
|
|
24
32
|
return null;
|
|
25
|
-
const
|
|
33
|
+
const types = declared.filter((entry) => typeof entry === "string");
|
|
34
|
+
return types.length > 0 && types.every((type) => COERCIBLE_TYPES.has(type)) ? types : null;
|
|
35
|
+
};
|
|
36
|
+
const bareScalarBranches = (branches) => {
|
|
26
37
|
if (!Array.isArray(branches) || branches.length === 0)
|
|
27
38
|
return null;
|
|
28
39
|
const types = [];
|
|
29
40
|
for (const branch of branches) {
|
|
30
41
|
if (!isSchemaObject(branch))
|
|
31
42
|
return null;
|
|
32
|
-
const
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
else if (readKey(branch, "type") === "object")
|
|
38
|
-
continue;
|
|
39
|
-
else if (readKey(branch, "type") === "array")
|
|
40
|
-
continue;
|
|
41
|
-
else
|
|
43
|
+
const node = branch;
|
|
44
|
+
const type = soleScalarType(node);
|
|
45
|
+
if (type === null)
|
|
46
|
+
return null;
|
|
47
|
+
if (Object.keys(node).some((key) => key !== "type" && !ANNOTATION_KEYWORDS.has(key)))
|
|
42
48
|
return null;
|
|
49
|
+
types.push(type);
|
|
50
|
+
}
|
|
51
|
+
return types;
|
|
52
|
+
};
|
|
53
|
+
const emitMatcher = (body, ctx) => {
|
|
54
|
+
const name = `_matches${ctx.next()}`;
|
|
55
|
+
const parameter = /\binput\b/.test(body) ? "input" : "_input";
|
|
56
|
+
const path = /\b_path\b/.test(body) ? [` const _path = ''`] : [];
|
|
57
|
+
ctx.helpers.push([`const ${name} = (${parameter}: unknown): boolean => {`, ...path, body, `}`].join("\n"));
|
|
58
|
+
return name;
|
|
59
|
+
};
|
|
60
|
+
const branchTest = (branch, ctx) => {
|
|
61
|
+
const named = ctx.matcher.named(branch);
|
|
62
|
+
if (named !== null)
|
|
63
|
+
return named;
|
|
64
|
+
const body = ctx.matcher.test(branch);
|
|
65
|
+
return typeof body === "string" ? emitMatcher(body, ctx) : body;
|
|
66
|
+
};
|
|
67
|
+
const emitBranchCoercer = (schema, keyword, ctx) => {
|
|
68
|
+
const branches = readKey(schema, keyword);
|
|
69
|
+
if (!Array.isArray(branches) || branches.length === 0)
|
|
70
|
+
return null;
|
|
71
|
+
const tests = [];
|
|
72
|
+
for (const branch of branches) {
|
|
73
|
+
const test = branchTest(branch, ctx);
|
|
74
|
+
if (test === true)
|
|
75
|
+
return null;
|
|
76
|
+
tests.push(test);
|
|
77
|
+
}
|
|
78
|
+
const live = branches.map((branch, index) => ({ name: tests[index], coerce: coercerFor(branch, ctx) })).filter((entry) => typeof entry.name === "string");
|
|
79
|
+
if (live.every(({ coerce }) => coerce === null))
|
|
80
|
+
return null;
|
|
81
|
+
const names = [];
|
|
82
|
+
const attempts = [];
|
|
83
|
+
for (const { name: name2, coerce } of live) {
|
|
84
|
+
names.push(name2);
|
|
85
|
+
if (coerce !== null)
|
|
86
|
+
attempts.push({ coerce, test: name2 });
|
|
87
|
+
}
|
|
88
|
+
const id = ctx.next();
|
|
89
|
+
const name = `_coerceBranches${id}`;
|
|
90
|
+
const lines = [
|
|
91
|
+
`const ${name} = (input: unknown): unknown => {`,
|
|
92
|
+
` if (${names.map((name2) => `${name2}(input)`).join(" || ")}) return input`
|
|
93
|
+
];
|
|
94
|
+
if (attempts.length === 1) {
|
|
95
|
+
const [only] = attempts;
|
|
96
|
+
lines.push(` const next = ${only.coerce("input")}`, ` return next !== input && ${only.test}(next) ? next : input`);
|
|
97
|
+
} else {
|
|
98
|
+
lines.push(` let out: unknown = input`);
|
|
99
|
+
attempts.forEach(({ coerce, test }, index) => {
|
|
100
|
+
const local = `_next${index}`;
|
|
101
|
+
lines.push(` const ${local} = ${coerce("input")}`, ` if (${local} !== input && ${test}(${local})) {`, ` if (out === input) out = ${local}`, ` else if (!valuesEqual(out, ${local})) return input`, ` }`);
|
|
102
|
+
});
|
|
103
|
+
lines.push(` return out`);
|
|
43
104
|
}
|
|
44
|
-
|
|
105
|
+
lines.push(`}`);
|
|
106
|
+
ctx.helpers.push(lines.join("\n"));
|
|
107
|
+
return (valueExpr) => `${name}(${valueExpr})`;
|
|
108
|
+
};
|
|
109
|
+
const emitUnionCoercer = (schema, keyword, ctx) => {
|
|
110
|
+
const scalars = bareScalarBranches(readKey(schema, keyword));
|
|
111
|
+
if (scalars !== null)
|
|
112
|
+
return (valueExpr) => `coerceUnion(${valueExpr}, ${JSON.stringify(scalars)})`;
|
|
113
|
+
return emitBranchCoercer(schema, keyword, ctx);
|
|
114
|
+
};
|
|
115
|
+
const emitAllOfCoercer = (schema, ctx) => {
|
|
116
|
+
const subschemas = readKey(schema, "allOf");
|
|
117
|
+
if (!Array.isArray(subschemas))
|
|
118
|
+
return null;
|
|
119
|
+
return compose(subschemas.map((sub) => coercerFor(sub, ctx)));
|
|
120
|
+
};
|
|
121
|
+
const emitConditionalCoercer = (schema, ctx) => {
|
|
122
|
+
if (!declaresKey(schema, "if"))
|
|
123
|
+
return null;
|
|
124
|
+
const thenCoercer = declaresKey(schema, "then") ? coercerFor(readKey(schema, "then"), ctx) : null;
|
|
125
|
+
const elseCoercer = declaresKey(schema, "else") ? coercerFor(readKey(schema, "else"), ctx) : null;
|
|
126
|
+
if (thenCoercer === null && elseCoercer === null)
|
|
127
|
+
return null;
|
|
128
|
+
const test = branchTest(readKey(schema, "if"), ctx);
|
|
129
|
+
if (test === true)
|
|
130
|
+
return thenCoercer;
|
|
131
|
+
if (test === false)
|
|
132
|
+
return elseCoercer;
|
|
133
|
+
const name = `_coerceIf${ctx.next()}`;
|
|
134
|
+
const whenThen = thenCoercer === null ? "input" : thenCoercer("input");
|
|
135
|
+
const whenElse = elseCoercer === null ? "input" : elseCoercer("input");
|
|
136
|
+
ctx.helpers.push(`const ${name} = (input: unknown): unknown => (${test}(input) ? ${whenThen} : ${whenElse})`);
|
|
137
|
+
return (valueExpr) => `${name}(${valueExpr})`;
|
|
138
|
+
};
|
|
139
|
+
const compose = (stages) => {
|
|
140
|
+
const present = stages.filter((stage) => stage !== null);
|
|
141
|
+
if (present.length === 0)
|
|
142
|
+
return null;
|
|
143
|
+
return (valueExpr) => present.reduce((expr, stage) => stage(expr), valueExpr);
|
|
45
144
|
};
|
|
46
145
|
const emitObjectCoercer = (schema, ctx) => {
|
|
47
146
|
const properties = schemaMap(schema, "properties") ?? {};
|
|
@@ -74,17 +173,20 @@ const emitObjectCoercer = (schema, ctx) => {
|
|
|
74
173
|
const quoted = JSON.stringify(key);
|
|
75
174
|
const local = `_value${id}_${index}`;
|
|
76
175
|
lines.push(
|
|
77
|
-
// Own properties only. A plain `obj[key]`
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
// `flag
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
|
|
176
|
+
// Own properties only, where it matters: at the write. A plain `obj[key]`
|
|
177
|
+
// walks the prototype chain, and this pass *writes* — with
|
|
178
|
+
// `Object.prototype.flag` set by any dependency, an empty object was read as
|
|
179
|
+
// carrying `flag`, coerced, and handed back with `flag` as its own,
|
|
180
|
+
// inventing data the caller never sent and satisfying a `required` the input
|
|
181
|
+
// actually violates. Coercing an inherited value is harmless (every walk is
|
|
182
|
+
// pure) as long as the result is never written, so the `Object.hasOwn` is
|
|
183
|
+
// asked only of a value that moved: on V8 it is the dearest thing in the
|
|
184
|
+
// walk, and most keys do not move. `{ a: undefined }` counts as absent
|
|
185
|
+
// everywhere else in this package, so it counts as absent here too.
|
|
186
|
+
` const ${local} = obj[${quoted}]`,
|
|
85
187
|
` if (${local} !== undefined) {`,
|
|
86
188
|
` const next = ${coerce(local)}`,
|
|
87
|
-
` if (next !== ${local}) (out ??= { ...obj })[${quoted}] = next`,
|
|
189
|
+
` if (next !== ${local} && Object.hasOwn(obj, ${quoted})) (out ??= { ...obj })[${quoted}] = next`,
|
|
88
190
|
` }`
|
|
89
191
|
);
|
|
90
192
|
});
|
|
@@ -143,17 +245,18 @@ const emitArrayCoercer = (schema, ctx) => {
|
|
|
143
245
|
return;
|
|
144
246
|
const read = `input[${index}]`;
|
|
145
247
|
lines.push(
|
|
146
|
-
// `Object.hasOwn` rather than a length test, for the same reason the
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
248
|
+
// `Object.hasOwn` rather than a length test, for the same reason the object
|
|
249
|
+
// walk uses it, and at the write for the same reason: a hole reads through
|
|
250
|
+
// `Array.prototype`, and writing what that coerces to would turn a hole into
|
|
251
|
+
// an own element.
|
|
252
|
+
` {`,
|
|
150
253
|
` const next = ${coerce(read)}`,
|
|
151
|
-
` if (next !== ${read}) (out ??= [...input])[${index}] = next`,
|
|
254
|
+
` if (next !== ${read} && Object.hasOwn(input, ${index})) (out ??= [...input])[${index}] = next`,
|
|
152
255
|
` }`
|
|
153
256
|
);
|
|
154
257
|
});
|
|
155
258
|
if (restCoercer !== null) {
|
|
156
|
-
lines.push(` for (let i = ${tuple.length}; i < input.length; i++) {`, `
|
|
259
|
+
lines.push(` for (let i = ${tuple.length}; i < input.length; i++) {`, ` const value = input[i]`, ` const next = ${restCoercer("value")}`, ` if (next !== value && Object.hasOwn(input, i)) (out ??= [...input])[i] = next`, ` }`);
|
|
157
260
|
}
|
|
158
261
|
lines.push(` return out ?? input`, `}`);
|
|
159
262
|
ctx.helpers.push(lines.join("\n"));
|
|
@@ -164,37 +267,61 @@ const coercerFor = (schema, ctx) => {
|
|
|
164
267
|
return null;
|
|
165
268
|
const node = schema;
|
|
166
269
|
const ref = readKey(node, "$ref");
|
|
167
|
-
if (typeof ref === "string") {
|
|
168
|
-
const name = coercerNameFor(ref, ctx.typeSuffix);
|
|
169
|
-
return (valueExpr) => `${name}(${valueExpr})`;
|
|
170
|
-
}
|
|
171
270
|
const scalar = soleScalarType(node);
|
|
172
|
-
|
|
173
|
-
return (valueExpr) => `coerceScalar(${valueExpr}, ${JSON.stringify(scalar)})`;
|
|
174
|
-
const union = unionScalarTypes(node);
|
|
175
|
-
if (union !== null)
|
|
176
|
-
return (valueExpr) => `coerceUnion(${valueExpr}, ${JSON.stringify(union)})`;
|
|
271
|
+
const typeArray = typeArrayScalars(node);
|
|
177
272
|
const type = readKey(node, "type");
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
273
|
+
return compose([
|
|
274
|
+
// Whether the target coerces anything is a question about another file, and
|
|
275
|
+
// following it here would have to chase cycles. The call is cheap and
|
|
276
|
+
// returns its argument when there is nothing to do.
|
|
277
|
+
typeof ref === "string" ? (valueExpr) => `${ctx.refCoercer(ref)}(${valueExpr})` : null,
|
|
278
|
+
scalar !== null ? (valueExpr) => `coerceScalar(${valueExpr}, ${JSON.stringify(scalar)})` : typeArray !== null ? (valueExpr) => `coerceUnion(${valueExpr}, ${JSON.stringify(typeArray)})` : type === "object" || type === void 0 && declaresObjectKeys(node) ? emitObjectCoercer(node, ctx) : type === "array" || type === void 0 && declaresArrayItems(node) ? emitArrayCoercer(node, ctx) : null,
|
|
279
|
+
emitAllOfCoercer(node, ctx),
|
|
280
|
+
emitUnionCoercer(node, "anyOf", ctx),
|
|
281
|
+
emitUnionCoercer(node, "oneOf", ctx),
|
|
282
|
+
emitConditionalCoercer(node, ctx)
|
|
283
|
+
]);
|
|
183
284
|
};
|
|
184
|
-
const
|
|
285
|
+
const generateCoerceWalk = (schema, options) => {
|
|
185
286
|
const helpers = [];
|
|
186
287
|
const hoisted = [];
|
|
187
288
|
let counter = 0;
|
|
188
|
-
const
|
|
289
|
+
const matcher = createSubschemaMatcher(options.typeSuffix, options.rootSchema ?? (isSchemaObject(schema) ? schema : void 0), options.formats ?? NO_FORMATS, options.hoistNamespace, ...options.refGuard !== void 0 ? [options.refGuard] : []);
|
|
290
|
+
const ctx = {
|
|
291
|
+
typeSuffix: options.typeSuffix,
|
|
292
|
+
helpers,
|
|
293
|
+
hoisted,
|
|
294
|
+
next: () => counter++,
|
|
295
|
+
matcher,
|
|
296
|
+
refCoercer: options.refCoercer
|
|
297
|
+
};
|
|
189
298
|
const coerce = coercerFor(schema, ctx);
|
|
299
|
+
const rootMatch = options.withRootMatch === true ? matcher.test(schema) : null;
|
|
300
|
+
const reads = helpers.join("\n") + (typeof rootMatch === "string" ? `
|
|
301
|
+
${rootMatch}` : "");
|
|
302
|
+
return {
|
|
303
|
+
declarations: [...hoisted, ...matcher.declarations(reads), ...helpers],
|
|
304
|
+
expression: coerce === null ? null : coerce("input"),
|
|
305
|
+
rootMatch
|
|
306
|
+
};
|
|
307
|
+
};
|
|
308
|
+
const generateCoerceFunction = (schema, typeName, typeSuffix = "", options = {}) => {
|
|
309
|
+
const { declarations, expression } = generateCoerceWalk(schema, {
|
|
310
|
+
typeSuffix,
|
|
311
|
+
...options.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
312
|
+
...options.formats !== void 0 ? { formats: options.formats } : {},
|
|
313
|
+
refCoercer: (ref) => coercerNameFor(ref, typeSuffix),
|
|
314
|
+
hoistNamespace: "_coerce"
|
|
315
|
+
});
|
|
190
316
|
const valueName = `coerce${typeName}Value`;
|
|
191
|
-
const body =
|
|
192
|
-
const
|
|
317
|
+
const body = expression ?? "input";
|
|
318
|
+
const passes = expression !== null && options.standaloneGuard === true ? `is${typeName}` : null;
|
|
193
319
|
const walk = [
|
|
194
|
-
...
|
|
320
|
+
...declarations.length > 0 ? [declarations.join("\n\n"), ""] : [],
|
|
195
321
|
`export const ${valueName} = (input: unknown): unknown => ${body}`,
|
|
196
322
|
"",
|
|
197
323
|
`export const coerce${typeName} = (input: unknown, _path = ''): CoercionResult<${typeName}> => {`,
|
|
324
|
+
...passes === null ? [] : [` if (${passes}(input)) return { valid: true, value: input as ${typeName} }`],
|
|
198
325
|
` const value = ${valueName}(input)`,
|
|
199
326
|
` const result = validate${typeName}(value, _path)`,
|
|
200
327
|
` return result === true ? { valid: true, value: value as ${typeName} } : { valid: false, errors: result.errors }`,
|
|
@@ -204,5 +331,6 @@ const generateCoerceFunction = (schema, typeName, typeSuffix = "") => {
|
|
|
204
331
|
};
|
|
205
332
|
export {
|
|
206
333
|
coercerNameFor,
|
|
207
|
-
generateCoerceFunction
|
|
334
|
+
generateCoerceFunction,
|
|
335
|
+
generateCoerceWalk
|
|
208
336
|
};
|
|
@@ -6,7 +6,7 @@ import { formatCheckName } from "./emit-format-checks.js";
|
|
|
6
6
|
import { NO_FORMATS } from "./enforced-keywords.js";
|
|
7
7
|
import { generateCoerceFunction } from "./generate-coerce-function.js";
|
|
8
8
|
import { generateRepairFunction } from "./generate-repair-function.js";
|
|
9
|
-
import {
|
|
9
|
+
import { generateBooleanGuardSource, generateCheckFunction, generateValidatorFunction } from "./generate-validator-function.js";
|
|
10
10
|
const generateValidatorFile = (schema, typeName, options) => {
|
|
11
11
|
const typeSuffix = options?.typeSuffix ?? "";
|
|
12
12
|
const typeDefinition = generateTypeDefinition(schema, typeName, {
|
|
@@ -17,10 +17,14 @@ const generateValidatorFile = (schema, typeName, options) => {
|
|
|
17
17
|
const importExt = options?.importExt ?? "js";
|
|
18
18
|
const formats = options?.formats ?? NO_FORMATS;
|
|
19
19
|
const validatorFunction = generateValidatorFunction(schema, typeName, typeSuffix, options?.rootSchema, unknownKeys, formats, options?.branchErrors === true);
|
|
20
|
-
const booleanGuard =
|
|
20
|
+
const { code: booleanGuard, standalone: standaloneGuard } = generateBooleanGuardSource(schema, typeName, unknownKeys, formats);
|
|
21
21
|
const checker = options?.check === true ? generateCheckFunction(schema, typeName, typeSuffix, options?.rootSchema, unknownKeys, formats) : "";
|
|
22
22
|
const wantsCoerce = options?.coerce === true || options?.repair === true;
|
|
23
|
-
const coercer = wantsCoerce ? generateCoerceFunction(schema, typeName, typeSuffix
|
|
23
|
+
const coercer = wantsCoerce ? generateCoerceFunction(schema, typeName, typeSuffix, {
|
|
24
|
+
...options?.rootSchema !== void 0 ? { rootSchema: options.rootSchema } : {},
|
|
25
|
+
formats,
|
|
26
|
+
standaloneGuard
|
|
27
|
+
}).code : "";
|
|
24
28
|
const repairer = options?.repair === true ? generateRepairFunction(schema, typeName, typeSuffix).code : "";
|
|
25
29
|
const appended = [checker, coercer, repairer].filter((part) => part !== "");
|
|
26
30
|
const body = validatorFunction + booleanGuard + (appended.length === 0 ? "" : "\n\n" + appended.join("\n\n"));
|
|
@@ -30,9 +34,10 @@ const generateValidatorFile = (schema, typeName, options) => {
|
|
|
30
34
|
rootSchema: options?.rootSchema,
|
|
31
35
|
typeSuffix,
|
|
32
36
|
importExt,
|
|
33
|
-
reads: ({ typeName: name, validatorName, checkerName, coercerName, repairerName }) => ({
|
|
37
|
+
reads: ({ typeName: name, validatorName, guardName, checkerName, coercerName, repairerName }) => ({
|
|
34
38
|
type: mentions(name),
|
|
35
39
|
validator: mentions(validatorName),
|
|
40
|
+
guard: mentions(guardName),
|
|
36
41
|
checker: mentions(checkerName),
|
|
37
42
|
coercer: mentions(coercerName),
|
|
38
43
|
repairer: mentions(repairerName)
|
|
@@ -17,6 +17,19 @@ import type { JSONSchema } from 'json-schema-typed/draft-2020-12';
|
|
|
17
17
|
* {@link UnknownKeysStrategy}.
|
|
18
18
|
*/
|
|
19
19
|
export declare const generateBooleanGuard: (schema: JSONSchema, typeName: string, _suffix?: string, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>) => string;
|
|
20
|
+
/**
|
|
21
|
+
* {@link generateBooleanGuard}, together with whether the guard it emitted stands
|
|
22
|
+
* on its own or falls back to calling `validateX`.
|
|
23
|
+
*
|
|
24
|
+
* The coercing half asks: it answers an already-valid document with one guard
|
|
25
|
+
* call, which is only a saving when the guard is a real fail-fast check. A
|
|
26
|
+
* fallback runs the whole validator and builds every error on the way to `false`,
|
|
27
|
+
* so there the coercer walks first instead.
|
|
28
|
+
*/
|
|
29
|
+
export declare const generateBooleanGuardSource: (schema: JSONSchema, typeName: string, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>) => {
|
|
30
|
+
code: string;
|
|
31
|
+
standalone: boolean;
|
|
32
|
+
};
|
|
20
33
|
/**
|
|
21
34
|
* Generates a TypeScript validator function from a JSON Schema.
|
|
22
35
|
*
|
|
@@ -56,3 +69,38 @@ export declare const generateValidatorFunction: (schema: JSONSchema, typeName: s
|
|
|
56
69
|
* way; only the short circuit is lost.
|
|
57
70
|
*/
|
|
58
71
|
export declare const generateCheckFunction: (schema: JSONSchema, typeName: string, suffix?: string, rootSchema?: Record<string, unknown>, unknownKeys?: UnknownKeysStrategy, formats?: ReadonlySet<string>) => string;
|
|
72
|
+
/**
|
|
73
|
+
* A yes/no matcher for subschemas, for generated code that has to ask "does this
|
|
74
|
+
* value satisfy that branch?" outside any validator body.
|
|
75
|
+
*
|
|
76
|
+
* The coercing half is the caller: a union only knows which branch a value was
|
|
77
|
+
* meant for by asking each branch, and the answer has to be the one `validateX`
|
|
78
|
+
* gives, or coercion would steer a value toward a branch the validator then
|
|
79
|
+
* rejects. So this hands out the very match expression the `anyOf` / `oneOf`
|
|
80
|
+
* emitters use, rather than a second opinion.
|
|
81
|
+
*
|
|
82
|
+
* Every expression built from one matcher shares one hoisted list, so the names
|
|
83
|
+
* it mints are unique across all of them; `hoistNamespace` keeps those names
|
|
84
|
+
* apart from the validator's own in the same module. `declarations` returns the
|
|
85
|
+
* hoisted lines a finished text actually reads, pruned the way
|
|
86
|
+
* {@link withHoisted} prunes a validator's.
|
|
87
|
+
*/
|
|
88
|
+
export declare const createSubschemaMatcher: (suffix: string, rootSchema: Record<string, unknown> | undefined, formats: ReadonlySet<string>, hoistNamespace: string, refGuard?: (typeName: string) => string) => {
|
|
89
|
+
/**
|
|
90
|
+
* The test for `sub` as the body of a function whose parameter is `input` —
|
|
91
|
+
* statements that `return false` at the first failure and `true` at the end —
|
|
92
|
+
* or a constant when the answer never depends on the value.
|
|
93
|
+
*
|
|
94
|
+
* A body rather than an expression because an expression here is an IIFE, and
|
|
95
|
+
* JavaScriptCore allocates the closure on every call: roughly 50 ns per branch
|
|
96
|
+
* tested, which on a union-heavy document was most of what coercing it cost.
|
|
97
|
+
*/
|
|
98
|
+
test: (sub: JSONSchema) => boolean | string;
|
|
99
|
+
/**
|
|
100
|
+
* The same test as a named function, hoisted with the rest and shared with
|
|
101
|
+
* every other test of the same subschema in this file — or a constant, or
|
|
102
|
+
* `null` for a body only the buffered form of {@link test} can express.
|
|
103
|
+
*/
|
|
104
|
+
named: (sub: JSONSchema) => boolean | string | null;
|
|
105
|
+
declarations: (text: string) => string[];
|
|
106
|
+
};
|