@webergency-utils/typechecker 0.1.9 → 0.2.1
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/LICENSE +1 -1
- package/README.md +307 -277
- package/dist/engine/generators.d.ts +2 -1
- package/dist/engine/generators.js +37 -11
- package/dist/engine/resolver.js +279 -68
- package/dist/engine/staticAsserts.js +3 -2
- package/dist/index.d.ts +12 -6
- package/dist/index.js +19 -0
- package/dist/plugin.d.ts +7 -0
- package/dist/plugin.js +30 -0
- package/dist/runtime/casing.d.ts +3 -3
- package/dist/runtime/casing.js +4 -4
- package/dist/runtime/tags/format.d.ts +7 -0
- package/dist/runtime/tags/tag.d.ts +1 -1
- package/dist/runtime/tags.d.ts +1 -1
- package/dist/runtime/validators.d.ts +34 -10
- package/dist/runtime/validators.js +915 -212
- package/dist/transformer.js +5 -1
- package/package.json +21 -6
|
@@ -11,7 +11,7 @@ export declare function createLiteralCheck(value: string | number | boolean | ts
|
|
|
11
11
|
export declare function createArrayCheck(elementValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
|
|
12
12
|
export declare function createTemplateLiteralCheck(regexStr: string, expected: string, requiredUtils: Set<string>): ts.Expression;
|
|
13
13
|
export declare function createUnionCheck(checks: ts.Expression[], requiredUtils: Set<string>, expected?: string): ts.Expression;
|
|
14
|
-
export declare function createObjectCheck(props: any[], requiredUtils: Set<string>, expected?: string): ts.Expression;
|
|
14
|
+
export declare function createObjectCheck(props: any[], requiredUtils: Set<string>, expected?: string, indexValidator?: ts.Expression): ts.Expression;
|
|
15
15
|
export declare function createRecordCheck(valueValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
|
|
16
16
|
export declare function createTupleCheck(checks: ts.Expression[], requiredUtils: Set<string>): ts.Expression;
|
|
17
17
|
export declare function createDateCheck(requiredUtils: Set<string>): ts.Expression;
|
|
@@ -21,3 +21,4 @@ export declare function createUndefinedCheck(requiredUtils: Set<string>): ts.Exp
|
|
|
21
21
|
export declare function createIntersectionCheck(checks: ts.Expression[], requiredUtils: Set<string>): ts.Expression;
|
|
22
22
|
export declare function createSetCheck(elementValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
|
|
23
23
|
export declare function createMapCheck(keyValidator: ts.Expression, valueValidator: ts.Expression, requiredUtils: Set<string>): ts.Expression;
|
|
24
|
+
export declare function createInstanceOfCheck(typeName: string, requiredUtils: Set<string>): ts.Expression;
|
|
@@ -79,7 +79,7 @@ export function createConstrainedPrimitiveCheck(baseType, constraints, requiredU
|
|
|
79
79
|
return `validators.pattern(v, path, ctx, new RegExp(${JSON.stringify(c.value)}), ${JSON.stringify('Pattern<' + c.value + '>')}${msgArg})`;
|
|
80
80
|
}
|
|
81
81
|
if (c.type === 'format') {
|
|
82
|
-
return `validators.format(v, path, ctx, ${JSON.stringify(c.value)}${msgArg})`;
|
|
82
|
+
return `v = validators.format(v, path, ctx, ${JSON.stringify(c.value)}${msgArg})`;
|
|
83
83
|
}
|
|
84
84
|
if (c.type === 'minItems') {
|
|
85
85
|
return `validators.minItems(v, path, ctx, ${valStr}${msgArg})`;
|
|
@@ -118,13 +118,13 @@ export function createConstrainedPrimitiveCheck(baseType, constraints, requiredU
|
|
|
118
118
|
return 'if (typeof v === \'string\' && v.length > 0) v = v.charAt(0).toUpperCase() + v.slice(1)';
|
|
119
119
|
}
|
|
120
120
|
if (tc.type === 'transform' && tc.value === 'tonumber') {
|
|
121
|
-
return 'v =
|
|
121
|
+
return 'v = validators.coerceQueryNumber(v)';
|
|
122
122
|
}
|
|
123
123
|
if (tc.type === 'transform' && tc.value === 'toboolean') {
|
|
124
|
-
return 'v = (v
|
|
124
|
+
return 'v = validators.coerceQueryBoolean(v)';
|
|
125
125
|
}
|
|
126
126
|
if (tc.type === 'transform' && tc.value === 'todate') {
|
|
127
|
-
return 'v =
|
|
127
|
+
return 'v = validators.coerceQueryDate(v)';
|
|
128
128
|
}
|
|
129
129
|
if (tc.type === 'transform_custom') {
|
|
130
130
|
return `v = ${tc.value}(v)`;
|
|
@@ -188,19 +188,39 @@ export function createUnionCheck(checks, requiredUtils, expected = 'Type<Union>'
|
|
|
188
188
|
ts.factory.createStringLiteral(expected)
|
|
189
189
|
]));
|
|
190
190
|
}
|
|
191
|
-
export function createObjectCheck(props, requiredUtils, expected = 'object') {
|
|
191
|
+
export function createObjectCheck(props, requiredUtils, expected = 'object', indexValidator) {
|
|
192
192
|
requiredUtils.add('validators');
|
|
193
|
-
const propDefinitions = props.map((p
|
|
193
|
+
const propDefinitions = props.map((p) => ts.factory.createArrayLiteralExpression([
|
|
194
194
|
ts.factory.createStringLiteral(p.name),
|
|
195
195
|
p.isOptional ? ts.factory.createTrue() : ts.factory.createFalse(),
|
|
196
196
|
p.validator
|
|
197
197
|
]));
|
|
198
198
|
const allowedKeys = props.map(p => ts.factory.createStringLiteral(p.name));
|
|
199
|
+
if (indexValidator) {
|
|
200
|
+
const tpl = `
|
|
201
|
+
(v, path, ctx) => {
|
|
202
|
+
const obj = validators.object(v, path, ctx, undefined, __EXPECTED__);
|
|
203
|
+
if (obj === false) return v;
|
|
204
|
+
const data = validators.objectShell(obj, ctx);
|
|
205
|
+
validators.props(obj, data, path, ctx, __PROPS__);
|
|
206
|
+
validators.additionalProps(obj, data, path, ctx, __KEYS__, __INDEX__);
|
|
207
|
+
return data;
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
return injectNodes(templateToAst(tpl), {
|
|
211
|
+
'__KEYS__': ts.factory.createArrayLiteralExpression(allowedKeys),
|
|
212
|
+
'__EXPECTED__': ts.factory.createStringLiteral(expected),
|
|
213
|
+
'__PROPS__': ts.factory.createArrayLiteralExpression(propDefinitions, true),
|
|
214
|
+
'__INDEX__': indexValidator
|
|
215
|
+
});
|
|
216
|
+
}
|
|
199
217
|
const tpl = `
|
|
200
218
|
(v, path, ctx) => {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
validators.
|
|
219
|
+
const obj = validators.object(v, path, ctx, __KEYS__, __EXPECTED__);
|
|
220
|
+
if (obj === false) return v;
|
|
221
|
+
const data = validators.objectShell(obj, ctx);
|
|
222
|
+
validators.props(obj, data, path, ctx, __PROPS__);
|
|
223
|
+
validators.stripExtras(data, ctx, __KEYS__);
|
|
204
224
|
return data;
|
|
205
225
|
}
|
|
206
226
|
`;
|
|
@@ -243,10 +263,11 @@ export function createIntersectionCheck(checks, requiredUtils) {
|
|
|
243
263
|
const tpl = `
|
|
244
264
|
(v, path, ctx) => {
|
|
245
265
|
const checks = __CHECKS__;
|
|
246
|
-
let data =
|
|
266
|
+
let data = validators.objectShell(v, ctx);
|
|
247
267
|
for (let i = 0; i < checks.length; i++) {
|
|
248
268
|
const val = checks[i](v, path, ctx);
|
|
249
|
-
if (
|
|
269
|
+
if (typeof val === "object" && val !== null && !Array.isArray(val) && typeof data === "object" && data !== null && !Array.isArray(data)) Object.assign(data, val);
|
|
270
|
+
else data = val;
|
|
250
271
|
}
|
|
251
272
|
return data;
|
|
252
273
|
}
|
|
@@ -265,3 +286,8 @@ export function createMapCheck(keyValidator, valueValidator, requiredUtils) {
|
|
|
265
286
|
const tpl = '(v, path, ctx) => validators.map(v, path, ctx, __KEY__, __VALUE__)';
|
|
266
287
|
return injectNodes(templateToAst(tpl), { '__KEY__': keyValidator, '__VALUE__': valueValidator });
|
|
267
288
|
}
|
|
289
|
+
export function createInstanceOfCheck(typeName, requiredUtils) {
|
|
290
|
+
requiredUtils.add('validators');
|
|
291
|
+
const tpl = `(v, path, ctx) => validators.instanceOf(v, path, ctx, ${JSON.stringify(typeName)})`;
|
|
292
|
+
return templateToAst(tpl);
|
|
293
|
+
}
|
package/dist/engine/resolver.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import ts from '../ts.js';
|
|
2
|
-
import { createPrimitiveCheck, createLiteralCheck, createArrayCheck, createUnionCheck, createObjectCheck, createDateCheck, createNullCheck, createUndefinedCheck, createIntersectionCheck, createTupleCheck, createRecordCheck, createRegExpCheck, createTemplateLiteralCheck, createConstrainedPrimitiveCheck, createSetCheck, createMapCheck } from './generators.js';
|
|
2
|
+
import { createPrimitiveCheck, createLiteralCheck, createArrayCheck, createUnionCheck, createObjectCheck, createDateCheck, createNullCheck, createUndefinedCheck, createIntersectionCheck, createTupleCheck, createRecordCheck, createRegExpCheck, createTemplateLiteralCheck, createConstrainedPrimitiveCheck, createSetCheck, createMapCheck, createInstanceOfCheck } from './generators.js';
|
|
3
3
|
import { createHash } from 'crypto';
|
|
4
4
|
function getStringLiteralValue(type) {
|
|
5
5
|
if (type.isStringLiteral()) {
|
|
@@ -48,6 +48,87 @@ function minifyTypeString(str) {
|
|
|
48
48
|
.replace(/:\s+/g, ':')
|
|
49
49
|
.replace(/\s+\|\s+/g, '|');
|
|
50
50
|
}
|
|
51
|
+
function isNativeEnumType(type) {
|
|
52
|
+
const flags = type.getFlags();
|
|
53
|
+
if (flags & ts.TypeFlags.Enum) {
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
const symbol = type.getSymbol();
|
|
57
|
+
if (!symbol) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return (symbol.flags & (ts.SymbolFlags.RegularEnum | ts.SymbolFlags.ConstEnum)) !== 0;
|
|
61
|
+
}
|
|
62
|
+
function buildEnumValidator(type, checker, validatorsMap, requiredUtils) {
|
|
63
|
+
const symbol = type.getSymbol();
|
|
64
|
+
const checks = [];
|
|
65
|
+
if (symbol?.exports) {
|
|
66
|
+
symbol.exports.forEach(member => {
|
|
67
|
+
if (!(member.flags & ts.SymbolFlags.EnumMember)) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const declaration = member.valueDeclaration || member.declarations?.[0];
|
|
71
|
+
if (!declaration) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const memberType = checker.getTypeOfSymbolAtLocation(member, declaration);
|
|
75
|
+
checks.push(buildValidator(memberType, checker, validatorsMap, requiredUtils));
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (checks.length === 0 && type.isUnion()) {
|
|
79
|
+
const unionChecks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
|
|
80
|
+
return createUnionCheck(unionChecks, requiredUtils, `Type<${minifyTypeString(checker.typeToString(type))}>`);
|
|
81
|
+
}
|
|
82
|
+
if (checks.length === 0) {
|
|
83
|
+
return createPrimitiveCheck('any', requiredUtils);
|
|
84
|
+
}
|
|
85
|
+
if (checks.length === 1) {
|
|
86
|
+
return checks[0];
|
|
87
|
+
}
|
|
88
|
+
return createUnionCheck(checks, requiredUtils, `Type<${minifyTypeString(checker.typeToString(type))}>`);
|
|
89
|
+
}
|
|
90
|
+
function isConstraintOnlyType(type, checker) {
|
|
91
|
+
const props = checker.getPropertiesOfType(type);
|
|
92
|
+
return props.length > 0 && props.every(p => p.getName().startsWith('__'));
|
|
93
|
+
}
|
|
94
|
+
function tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils, expected) {
|
|
95
|
+
const objectTypes = types.filter(t => {
|
|
96
|
+
if (isConstraintOnlyType(t, checker)) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const flags = t.getFlags();
|
|
100
|
+
return ((flags & ts.TypeFlags.Object) !== 0) || t.isClassOrInterface();
|
|
101
|
+
});
|
|
102
|
+
if (objectTypes.length < 2) {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
const others = types.filter(t => !isConstraintOnlyType(t, checker) && !objectTypes.includes(t));
|
|
106
|
+
if (others.length > 0) {
|
|
107
|
+
return undefined;
|
|
108
|
+
}
|
|
109
|
+
const propMap = new Map();
|
|
110
|
+
let indexValidator;
|
|
111
|
+
for (const t of objectTypes) {
|
|
112
|
+
const stringIndexInfo = checker.getIndexInfoOfType(t, ts.IndexKind.String);
|
|
113
|
+
if (stringIndexInfo) {
|
|
114
|
+
indexValidator = buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils);
|
|
115
|
+
}
|
|
116
|
+
for (const prop of checker.getPropertiesOfType(t)) {
|
|
117
|
+
const name = prop.getName();
|
|
118
|
+
if (name.startsWith('__')) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const declaration = prop.valueDeclaration || prop.declarations?.[0];
|
|
122
|
+
const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
|
|
123
|
+
propMap.set(name, {
|
|
124
|
+
name,
|
|
125
|
+
isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
|
|
126
|
+
validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return createObjectCheck([...propMap.values()], requiredUtils, expected, indexValidator);
|
|
131
|
+
}
|
|
51
132
|
export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
52
133
|
const hash = generateHash(type, checker);
|
|
53
134
|
if (validatorsMap.has(hash)) {
|
|
@@ -159,7 +240,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
159
240
|
}
|
|
160
241
|
}
|
|
161
242
|
if (fnName === '__function' || !fnName) {
|
|
162
|
-
throw new Error('[Webergency] Custom
|
|
243
|
+
throw new Error('[Webergency] Custom transform must reference a named function via typeof (e.g. transform.Custom<typeof myFunc>).');
|
|
163
244
|
}
|
|
164
245
|
if (filePath) {
|
|
165
246
|
requiredUtils.add(`custom:${fnName}:${filePath}`);
|
|
@@ -199,7 +280,7 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
199
280
|
}
|
|
200
281
|
}
|
|
201
282
|
if (fnName === '__function' || !fnName) {
|
|
202
|
-
throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g.
|
|
283
|
+
throw new Error('[Webergency] Custom validator must reference a named function via typeof (e.g. constraint.Custom<typeof myFunc>).');
|
|
203
284
|
}
|
|
204
285
|
if (filePath) {
|
|
205
286
|
requiredUtils.add(`custom:${fnName}:${filePath}`);
|
|
@@ -306,21 +387,27 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
306
387
|
baseValidator = buildValidator(nonConstraintTypes[0], checker, validatorsMap, requiredUtils);
|
|
307
388
|
}
|
|
308
389
|
else if (nonConstraintTypes.length > 1) {
|
|
309
|
-
const
|
|
310
|
-
baseValidator = createIntersectionCheck(
|
|
390
|
+
const merged = tryMergeObjectIntersection(nonConstraintTypes, checker, validatorsMap, requiredUtils, minifyTypeString(checker.typeToString(type)));
|
|
391
|
+
baseValidator = merged || createIntersectionCheck(nonConstraintTypes.map(t => buildValidator(t, checker, validatorsMap, requiredUtils)), requiredUtils);
|
|
311
392
|
}
|
|
312
393
|
if (baseValidator) {
|
|
313
394
|
result = createConstrainedPrimitiveCheck('any', constraints, requiredUtils, baseValidator);
|
|
314
395
|
}
|
|
315
396
|
else {
|
|
316
|
-
const
|
|
317
|
-
result = createIntersectionCheck(
|
|
397
|
+
const merged = tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils, minifyTypeString(checker.typeToString(type)));
|
|
398
|
+
result = merged || createIntersectionCheck(type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils)), requiredUtils);
|
|
318
399
|
}
|
|
319
400
|
}
|
|
320
401
|
}
|
|
321
402
|
else {
|
|
322
|
-
const
|
|
323
|
-
|
|
403
|
+
const merged = tryMergeObjectIntersection(types, checker, validatorsMap, requiredUtils, minifyTypeString(checker.typeToString(type)));
|
|
404
|
+
if (merged) {
|
|
405
|
+
result = merged;
|
|
406
|
+
}
|
|
407
|
+
else {
|
|
408
|
+
const checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
|
|
409
|
+
result = createIntersectionCheck(checks, requiredUtils);
|
|
410
|
+
}
|
|
324
411
|
}
|
|
325
412
|
}
|
|
326
413
|
else if (type.getSymbol()?.name === 'Date') {
|
|
@@ -338,6 +425,15 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
338
425
|
const valueType = type.typeArguments?.[1] || checker.getAnyType();
|
|
339
426
|
result = createMapCheck(buildValidator(keyType, checker, validatorsMap, requiredUtils), buildValidator(valueType, checker, validatorsMap, requiredUtils), requiredUtils);
|
|
340
427
|
}
|
|
428
|
+
else if (type.getSymbol()?.name === 'Promise') {
|
|
429
|
+
result = createInstanceOfCheck('Promise', requiredUtils);
|
|
430
|
+
}
|
|
431
|
+
else if (type.getSymbol()?.name && [
|
|
432
|
+
'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array',
|
|
433
|
+
'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'
|
|
434
|
+
].includes(type.getSymbol().name)) {
|
|
435
|
+
result = createInstanceOfCheck(type.getSymbol().name, requiredUtils);
|
|
436
|
+
}
|
|
341
437
|
else if (flags & ts.TypeFlags.Null) {
|
|
342
438
|
result = createNullCheck(requiredUtils);
|
|
343
439
|
}
|
|
@@ -356,6 +452,12 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
356
452
|
else if (flags & ts.TypeFlags.Boolean) {
|
|
357
453
|
result = createPrimitiveCheck('boolean', requiredUtils);
|
|
358
454
|
}
|
|
455
|
+
else if (flags & ts.TypeFlags.Never) {
|
|
456
|
+
result = createPrimitiveCheck('never', requiredUtils);
|
|
457
|
+
}
|
|
458
|
+
else if (flags & ts.TypeFlags.ESSymbol || flags & ts.TypeFlags.UniqueESSymbol || type.intrinsicName === 'symbol') {
|
|
459
|
+
result = createPrimitiveCheck('symbol', requiredUtils);
|
|
460
|
+
}
|
|
359
461
|
else if (flags & ts.TypeFlags.TemplateLiteral) {
|
|
360
462
|
const templateType = type;
|
|
361
463
|
let regexStr = '^';
|
|
@@ -404,23 +506,32 @@ export function buildValidator(type, checker, validatorsMap, requiredUtils) {
|
|
|
404
506
|
const elementType = type.typeArguments?.[0] || checker.getAnyType();
|
|
405
507
|
result = createArrayCheck(buildValidator(elementType, checker, validatorsMap, requiredUtils), requiredUtils);
|
|
406
508
|
}
|
|
509
|
+
else if (type.getCallSignatures().length > 0 && type.getConstructSignatures().length === 0) {
|
|
510
|
+
result = createPrimitiveCheck('function', requiredUtils);
|
|
511
|
+
}
|
|
512
|
+
else if (isNativeEnumType(type)) {
|
|
513
|
+
result = buildEnumValidator(type, checker, validatorsMap, requiredUtils);
|
|
514
|
+
}
|
|
407
515
|
else {
|
|
408
516
|
const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
409
|
-
|
|
517
|
+
const props = checker.getPropertiesOfType(type).map(prop => {
|
|
518
|
+
const declaration = prop.valueDeclaration || prop.declarations?.[0];
|
|
519
|
+
const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
|
|
520
|
+
return {
|
|
521
|
+
name: prop.getName(),
|
|
522
|
+
isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
|
|
523
|
+
validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
if (stringIndexInfo && props.length === 0) {
|
|
410
527
|
result = createRecordCheck(buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils), requiredUtils);
|
|
411
528
|
}
|
|
412
|
-
else if (flags & ts.TypeFlags.Object || type.isClassOrInterface() || type.isTypeParameter()) {
|
|
413
|
-
const props = checker.getPropertiesOfType(type).map(prop => {
|
|
414
|
-
const declaration = prop.valueDeclaration || prop.declarations?.[0];
|
|
415
|
-
const propType = declaration ? checker.getTypeOfSymbolAtLocation(prop, declaration) : checker.getAnyType();
|
|
416
|
-
return {
|
|
417
|
-
name: prop.getName(),
|
|
418
|
-
isOptional: (prop.getFlags() & ts.SymbolFlags.Optional) !== 0,
|
|
419
|
-
validator: buildValidator(propType, checker, validatorsMap, requiredUtils)
|
|
420
|
-
};
|
|
421
|
-
});
|
|
529
|
+
else if (flags & ts.TypeFlags.Object || type.isClassOrInterface() || type.isTypeParameter() || stringIndexInfo) {
|
|
422
530
|
const typeName = checker.typeToString(type);
|
|
423
|
-
|
|
531
|
+
const indexValidator = stringIndexInfo
|
|
532
|
+
? buildValidator(stringIndexInfo.type, checker, validatorsMap, requiredUtils)
|
|
533
|
+
: undefined;
|
|
534
|
+
result = createObjectCheck(props, requiredUtils, typeName, indexValidator);
|
|
424
535
|
}
|
|
425
536
|
else {
|
|
426
537
|
result = createPrimitiveCheck('any', requiredUtils);
|
|
@@ -475,6 +586,38 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
|
475
586
|
if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
|
|
476
587
|
return 'undefined';
|
|
477
588
|
}
|
|
589
|
+
if (flags & ts.TypeFlags.Never) {
|
|
590
|
+
return 'never';
|
|
591
|
+
}
|
|
592
|
+
if (flags & ts.TypeFlags.Unknown) {
|
|
593
|
+
return 'unknown';
|
|
594
|
+
}
|
|
595
|
+
if (flags & ts.TypeFlags.Any) {
|
|
596
|
+
return 'any';
|
|
597
|
+
}
|
|
598
|
+
if (flags & ts.TypeFlags.ESSymbol || flags & ts.TypeFlags.UniqueESSymbol || type.intrinsicName === 'symbol') {
|
|
599
|
+
return 'symbol';
|
|
600
|
+
}
|
|
601
|
+
if (type.getCallSignatures().length > 0 && type.getConstructSignatures().length === 0) {
|
|
602
|
+
return 'function';
|
|
603
|
+
}
|
|
604
|
+
if (isNativeEnumType(type)) {
|
|
605
|
+
const symbol = type.getSymbol();
|
|
606
|
+
const members = [];
|
|
607
|
+
if (symbol?.exports) {
|
|
608
|
+
symbol.exports.forEach(member => {
|
|
609
|
+
if (!(member.flags & ts.SymbolFlags.EnumMember)) {
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const declaration = member.valueDeclaration || member.declarations?.[0];
|
|
613
|
+
if (!declaration) {
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
members.push(buildStructuralSignature(checker.getTypeOfSymbolAtLocation(member, declaration), checker, visited));
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
return `Enum<${symbol?.name || 'anonymous'}:${members.sort().join(',')}>`;
|
|
620
|
+
}
|
|
478
621
|
if (flags & ts.TypeFlags.TemplateLiteral) {
|
|
479
622
|
const templateType = type;
|
|
480
623
|
return `TemplateLiteral<${templateType.texts.join(',')}|${templateType.types.map(t => buildStructuralSignature(t, checker, visited)).join(',')}>`;
|
|
@@ -483,11 +626,37 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
|
483
626
|
const elementType = type.typeArguments?.[0] || checker.getAnyType();
|
|
484
627
|
return `Array<${buildStructuralSignature(elementType, checker, visited)}>`;
|
|
485
628
|
}
|
|
629
|
+
const typeSymbolName = type.getSymbol()?.name || type.aliasSymbol?.getName();
|
|
630
|
+
if (typeSymbolName === 'Date') {
|
|
631
|
+
return 'Date';
|
|
632
|
+
}
|
|
633
|
+
if (typeSymbolName === 'RegExp') {
|
|
634
|
+
return 'RegExp';
|
|
635
|
+
}
|
|
636
|
+
if (typeSymbolName === 'Promise') {
|
|
637
|
+
const valueType = type.typeArguments?.[0] || checker.getAnyType();
|
|
638
|
+
return `Promise<${buildStructuralSignature(valueType, checker, visited)}>`;
|
|
639
|
+
}
|
|
640
|
+
if (typeSymbolName === 'Set') {
|
|
641
|
+
const elementType = type.typeArguments?.[0] || checker.getAnyType();
|
|
642
|
+
return `Set<${buildStructuralSignature(elementType, checker, visited)}>`;
|
|
643
|
+
}
|
|
644
|
+
if (typeSymbolName === 'Map') {
|
|
645
|
+
const keyType = type.typeArguments?.[0] || checker.getAnyType();
|
|
646
|
+
const valueType = type.typeArguments?.[1] || checker.getAnyType();
|
|
647
|
+
return `Map<${buildStructuralSignature(keyType, checker, visited)},${buildStructuralSignature(valueType, checker, visited)}>`;
|
|
648
|
+
}
|
|
649
|
+
if (typeSymbolName && [
|
|
650
|
+
'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array',
|
|
651
|
+
'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'
|
|
652
|
+
].includes(typeSymbolName)) {
|
|
653
|
+
return typeSymbolName;
|
|
654
|
+
}
|
|
486
655
|
if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
|
|
487
656
|
const props = checker.getPropertiesOfType(type);
|
|
657
|
+
const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
488
658
|
if (props.length === 0) {
|
|
489
659
|
// Handle Record or empty object
|
|
490
|
-
const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
491
660
|
if (stringIndexInfo) {
|
|
492
661
|
return `Record<${buildStructuralSignature(stringIndexInfo.type, checker, visited)}>`;
|
|
493
662
|
}
|
|
@@ -498,7 +667,10 @@ function buildStructuralSignature(type, checker, visited = new Set()) {
|
|
|
498
667
|
const isOptional = (prop.getFlags() & ts.SymbolFlags.Optional) !== 0;
|
|
499
668
|
return `${prop.getName()}${isOptional ? '?' : ''}:${buildStructuralSignature(propType, checker, visited)}`;
|
|
500
669
|
}).sort();
|
|
501
|
-
|
|
670
|
+
const indexSig = stringIndexInfo
|
|
671
|
+
? `;[string]:${buildStructuralSignature(stringIndexInfo.type, checker, visited)}`
|
|
672
|
+
: '';
|
|
673
|
+
return `Object{${propSigs.join(';')}${indexSig}}`;
|
|
502
674
|
}
|
|
503
675
|
return 'any';
|
|
504
676
|
}
|
|
@@ -679,42 +851,14 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
679
851
|
const types = type.types;
|
|
680
852
|
let baseSchema = {};
|
|
681
853
|
const constraints = {};
|
|
854
|
+
const memberSchemas = [];
|
|
682
855
|
for (const sub of types) {
|
|
683
856
|
const sFlags = sub.getFlags();
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
}
|
|
690
|
-
else if (sFlags & ts.TypeFlags.BigInt) {
|
|
691
|
-
baseSchema = { type: 'integer' };
|
|
692
|
-
}
|
|
693
|
-
else if (sFlags & ts.TypeFlags.Boolean || sub.intrinsicName === 'boolean') {
|
|
694
|
-
baseSchema = { type: 'boolean' };
|
|
695
|
-
}
|
|
696
|
-
else if (sub.getSymbol()?.name === 'Date') {
|
|
697
|
-
baseSchema = { type: 'string', format: 'date-time' };
|
|
698
|
-
}
|
|
699
|
-
else if (sub.getSymbol()?.name === 'RegExp') {
|
|
700
|
-
baseSchema = { type: 'string', format: 'regex' };
|
|
701
|
-
}
|
|
702
|
-
else if (sub.getSymbol()?.name === 'Set') {
|
|
703
|
-
const elementType = sub.typeArguments?.[0] || checker.getAnyType();
|
|
704
|
-
baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes), uniqueItems: true };
|
|
705
|
-
}
|
|
706
|
-
else if (sub.getSymbol()?.name === 'Map') {
|
|
707
|
-
const valueType = sub.typeArguments?.[1] || checker.getAnyType();
|
|
708
|
-
baseSchema = { type: 'object', additionalProperties: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes) };
|
|
709
|
-
}
|
|
710
|
-
else if (checker.isArrayType(sub)) {
|
|
711
|
-
const elementType = sub.typeArguments?.[0] || checker.getAnyType();
|
|
712
|
-
baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes) };
|
|
713
|
-
}
|
|
714
|
-
const props = checker.getPropertiesOfType(sub);
|
|
715
|
-
for (const prop of props) {
|
|
716
|
-
const pName = prop.getName();
|
|
717
|
-
if (pName.startsWith('__')) {
|
|
857
|
+
const subProps = checker.getPropertiesOfType(sub);
|
|
858
|
+
const isConstraintPhantom = subProps.length > 0 && subProps.every(p => p.getName().startsWith('__'));
|
|
859
|
+
if (isConstraintPhantom) {
|
|
860
|
+
for (const prop of subProps) {
|
|
861
|
+
const pName = prop.getName();
|
|
718
862
|
const pType = checker.getTypeOfSymbolAtLocation(prop, prop.valueDeclaration || prop.declarations?.[0]);
|
|
719
863
|
const actualType = stripUndefinedFromType(pType);
|
|
720
864
|
const val = getTagPropertyValue(pType);
|
|
@@ -775,36 +919,97 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
775
919
|
constraints.requires = reqVal;
|
|
776
920
|
}
|
|
777
921
|
}
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
if (sFlags & ts.TypeFlags.String || sFlags & ts.TypeFlags.TemplateLiteral) {
|
|
925
|
+
baseSchema = { type: 'string' };
|
|
926
|
+
}
|
|
927
|
+
else if (sFlags & ts.TypeFlags.Number) {
|
|
928
|
+
baseSchema = { type: 'number' };
|
|
929
|
+
}
|
|
930
|
+
else if (sFlags & ts.TypeFlags.BigInt) {
|
|
931
|
+
baseSchema = { 'x-typescript-type': 'bigint' };
|
|
778
932
|
}
|
|
933
|
+
else if (sFlags & ts.TypeFlags.Boolean || sub.intrinsicName === 'boolean') {
|
|
934
|
+
baseSchema = { type: 'boolean' };
|
|
935
|
+
}
|
|
936
|
+
else if (sub.getSymbol()?.name === 'Date') {
|
|
937
|
+
baseSchema = { 'x-typescript-type': 'Date' };
|
|
938
|
+
}
|
|
939
|
+
else if (sub.getSymbol()?.name === 'RegExp') {
|
|
940
|
+
baseSchema = { 'x-typescript-type': 'RegExp' };
|
|
941
|
+
}
|
|
942
|
+
else if (sub.getSymbol()?.name === 'Set') {
|
|
943
|
+
const elementType = sub.typeArguments?.[0] || checker.getAnyType();
|
|
944
|
+
baseSchema = {
|
|
945
|
+
'x-typescript-type': 'Set',
|
|
946
|
+
items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes)
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
else if (sub.getSymbol()?.name === 'Map') {
|
|
950
|
+
const keyType = sub.typeArguments?.[0] || checker.getAnyType();
|
|
951
|
+
const valueType = sub.typeArguments?.[1] || checker.getAnyType();
|
|
952
|
+
baseSchema = {
|
|
953
|
+
'x-typescript-type': 'Map',
|
|
954
|
+
key: buildJsonSchemaInternal(keyType, checker, defs, visited, counts, circularHashes),
|
|
955
|
+
value: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
else if (checker.isArrayType(sub)) {
|
|
959
|
+
const elementType = sub.typeArguments?.[0] || checker.getAnyType();
|
|
960
|
+
baseSchema = { type: 'array', items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes) };
|
|
961
|
+
}
|
|
962
|
+
else if ((sFlags & ts.TypeFlags.Object) || sub.isClassOrInterface()) {
|
|
963
|
+
memberSchemas.push(buildJsonSchemaInternal(sub, checker, defs, visited, counts, circularHashes));
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (memberSchemas.length > 1) {
|
|
967
|
+
return { allOf: memberSchemas, ...constraints };
|
|
968
|
+
}
|
|
969
|
+
if (memberSchemas.length === 1) {
|
|
970
|
+
return { ...memberSchemas[0], ...constraints };
|
|
779
971
|
}
|
|
780
972
|
return { ...baseSchema, ...constraints };
|
|
781
973
|
}
|
|
782
974
|
if (type.getSymbol()?.name === 'Date') {
|
|
783
|
-
return { type
|
|
975
|
+
return { 'x-typescript-type': 'Date' };
|
|
784
976
|
}
|
|
785
977
|
if (type.getSymbol()?.name === 'RegExp') {
|
|
786
|
-
return { type
|
|
978
|
+
return { 'x-typescript-type': 'RegExp' };
|
|
979
|
+
}
|
|
980
|
+
if (type.getSymbol()?.name === 'Promise') {
|
|
981
|
+
return { 'x-typescript-type': 'Promise' };
|
|
982
|
+
}
|
|
983
|
+
{
|
|
984
|
+
const typedName = type.getSymbol()?.name;
|
|
985
|
+
if (typedName && [
|
|
986
|
+
'Uint8Array', 'Uint16Array', 'Uint32Array', 'Int8Array', 'Int16Array', 'Int32Array',
|
|
987
|
+
'Float32Array', 'Float64Array', 'ArrayBuffer', 'SharedArrayBuffer', 'DataView', 'Buffer'
|
|
988
|
+
].includes(typedName)) {
|
|
989
|
+
return { 'x-typescript-type': typedName };
|
|
990
|
+
}
|
|
787
991
|
}
|
|
788
992
|
if (type.getSymbol()?.name === 'Set') {
|
|
789
993
|
const elementType = type.typeArguments?.[0] || checker.getAnyType();
|
|
790
994
|
return {
|
|
791
|
-
type: '
|
|
792
|
-
items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes)
|
|
793
|
-
uniqueItems: true
|
|
995
|
+
'x-typescript-type': 'Set',
|
|
996
|
+
items: buildJsonSchemaInternal(elementType, checker, defs, visited, counts, circularHashes)
|
|
794
997
|
};
|
|
795
998
|
}
|
|
796
999
|
if (type.getSymbol()?.name === 'Map') {
|
|
1000
|
+
const keyType = type.typeArguments?.[0] || checker.getAnyType();
|
|
797
1001
|
const valueType = type.typeArguments?.[1] || checker.getAnyType();
|
|
798
1002
|
return {
|
|
799
|
-
type: '
|
|
800
|
-
|
|
1003
|
+
'x-typescript-type': 'Map',
|
|
1004
|
+
key: buildJsonSchemaInternal(keyType, checker, defs, visited, counts, circularHashes),
|
|
1005
|
+
value: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
|
|
801
1006
|
};
|
|
802
1007
|
}
|
|
803
1008
|
if (flags & ts.TypeFlags.Null) {
|
|
804
1009
|
return { type: 'null' };
|
|
805
1010
|
}
|
|
806
1011
|
if (flags & ts.TypeFlags.Undefined || flags & ts.TypeFlags.Void) {
|
|
807
|
-
return { type
|
|
1012
|
+
return { 'x-typescript-type': 'undefined' };
|
|
808
1013
|
}
|
|
809
1014
|
if (flags & ts.TypeFlags.String) {
|
|
810
1015
|
return { type: 'string' };
|
|
@@ -813,7 +1018,7 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
813
1018
|
return { type: 'number' };
|
|
814
1019
|
}
|
|
815
1020
|
if (flags & ts.TypeFlags.BigInt) {
|
|
816
|
-
return { type: '
|
|
1021
|
+
return { 'x-typescript-type': 'bigint' };
|
|
817
1022
|
}
|
|
818
1023
|
if (flags & ts.TypeFlags.Boolean || type.intrinsicName === 'boolean') {
|
|
819
1024
|
return { type: 'boolean' };
|
|
@@ -848,6 +1053,7 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
848
1053
|
}
|
|
849
1054
|
// Object types
|
|
850
1055
|
if (flags & ts.TypeFlags.Object || type.isClassOrInterface()) {
|
|
1056
|
+
const stringIndexInfo = checker.getIndexInfoOfType(type, ts.IndexKind.String);
|
|
851
1057
|
const symbol = type.getSymbol() || type.aliasSymbol;
|
|
852
1058
|
const name = symbol ? symbol.getName() : 'Object';
|
|
853
1059
|
const typeHash = generateHash(type, checker);
|
|
@@ -875,9 +1081,14 @@ function buildJsonSchemaInternal(type, checker, defs, visited, counts, circularH
|
|
|
875
1081
|
}
|
|
876
1082
|
const schemaObj = {
|
|
877
1083
|
type: 'object',
|
|
878
|
-
properties
|
|
879
|
-
additionalProperties: false
|
|
1084
|
+
properties
|
|
880
1085
|
};
|
|
1086
|
+
if (stringIndexInfo) {
|
|
1087
|
+
schemaObj.additionalProperties = buildJsonSchemaInternal(stringIndexInfo.type, checker, defs, visited, counts, circularHashes);
|
|
1088
|
+
}
|
|
1089
|
+
else {
|
|
1090
|
+
schemaObj.additionalProperties = false;
|
|
1091
|
+
}
|
|
881
1092
|
if (required.length > 0) {
|
|
882
1093
|
schemaObj.required = required;
|
|
883
1094
|
}
|
|
@@ -168,12 +168,13 @@ export function evaluateStaticConstraints(constant, constraints) {
|
|
|
168
168
|
else if (c.type === 'multipleOf' && constant.kind === 'number') {
|
|
169
169
|
const v = constant.value;
|
|
170
170
|
const n = c.value;
|
|
171
|
-
let ok
|
|
171
|
+
let ok;
|
|
172
172
|
if (typeof v === 'bigint' || typeof n === 'bigint') {
|
|
173
173
|
ok = BigInt(v) % BigInt(n) === 0n;
|
|
174
174
|
}
|
|
175
175
|
else {
|
|
176
|
-
|
|
176
|
+
const q = v / n;
|
|
177
|
+
ok = Math.abs(q - Math.round(q)) <= 1e-8 * Math.max(1, Math.abs(q));
|
|
177
178
|
}
|
|
178
179
|
if (!ok) {
|
|
179
180
|
errors.push(c.message || `Value ${String(v)} does not satisfy MultipleOf<${n}>`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
import { ResolveDefaults } from './runtime/tags.js';
|
|
2
|
+
import type { IValidationError } from './runtime/validators.js';
|
|
2
3
|
export interface IValidation<T> {
|
|
3
4
|
success: boolean;
|
|
4
5
|
data?: T;
|
|
5
|
-
errors?:
|
|
6
|
+
errors?: IValidationError[];
|
|
6
7
|
}
|
|
7
8
|
export type ValidationMode = 'strict' | 'relaxed' | 'strip';
|
|
8
9
|
export interface ValidationOptions {
|
|
9
10
|
mode?: ValidationMode;
|
|
10
|
-
|
|
11
|
+
from?: 'json' | 'query' | ((key: string, value: any, type: 'string' | 'number' | 'boolean' | 'bigint' | 'function' | 'symbol' | 'never' | 'Date' | 'RegExp' | 'Set' | 'Map' | 'Array' | 'Object' | 'instance' | 'null' | 'undefined' | 'tuple' | 'literal') => any);
|
|
11
12
|
wrapArrays?: boolean;
|
|
13
|
+
/** When true, write validated/coerced values onto the input. Default false: always return new containers. */
|
|
14
|
+
mutate?: boolean;
|
|
12
15
|
schema?: any;
|
|
13
16
|
errorFactory?: (errors: any[]) => Error;
|
|
14
17
|
}
|
|
15
|
-
|
|
16
|
-
export declare function
|
|
17
|
-
|
|
18
|
-
export declare function
|
|
18
|
+
/** Returns whether `input` already matches `T`. Does not coerce; `from` is ignored. */
|
|
19
|
+
export declare function is<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): _input is ResolveDefaults<T>;
|
|
20
|
+
/** Validates and returns the (possibly coerced) value. Use `from` when conversion is needed. */
|
|
21
|
+
export declare function assert<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): ResolveDefaults<T>;
|
|
22
|
+
/** Asserts `input` already matches `T`. Does not coerce; `from` is ignored. */
|
|
23
|
+
export declare function assertGuard<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): asserts _input is ResolveDefaults<T>;
|
|
24
|
+
export declare function validate<T>(_input: unknown, _options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>;
|
|
19
25
|
export declare function jsonSchema<T>(): any;
|
|
20
26
|
export * from './runtime/validators.js';
|
|
21
27
|
export * from './runtime/tags.js';
|