@webergency-utils/typechecker 0.1.10 → 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.
@@ -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 validator must reference a named function via typeof (e.g. tag.Custom<typeof myFunc>).');
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. tag.Custom<typeof myFunc>).');
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 checks = nonConstraintTypes.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
310
- baseValidator = createIntersectionCheck(checks, requiredUtils);
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 checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
317
- result = createIntersectionCheck(checks, requiredUtils);
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 checks = type.types.map(t => buildValidator(t, checker, validatorsMap, requiredUtils));
323
- result = createIntersectionCheck(checks, requiredUtils);
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
- if (stringIndexInfo) {
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
- result = createObjectCheck(props, requiredUtils, typeName);
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
- return `Object{${propSigs.join(';')}}`;
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
- if (sFlags & ts.TypeFlags.String || sFlags & ts.TypeFlags.TemplateLiteral) {
685
- baseSchema = { type: 'string' };
686
- }
687
- else if (sFlags & ts.TypeFlags.Number) {
688
- baseSchema = { type: 'number' };
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: 'string', format: 'date-time' };
975
+ return { 'x-typescript-type': 'Date' };
784
976
  }
785
977
  if (type.getSymbol()?.name === 'RegExp') {
786
- return { type: 'string', format: 'regex' };
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: 'array',
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: 'object',
800
- additionalProperties: buildJsonSchemaInternal(valueType, checker, defs, visited, counts, circularHashes)
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: 'null', description: 'undefined' };
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: 'integer' };
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
  }
@@ -173,7 +173,8 @@ export function evaluateStaticConstraints(constant, constraints) {
173
173
  ok = BigInt(v) % BigInt(n) === 0n;
174
174
  }
175
175
  else {
176
- ok = v % n === 0;
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?: any[];
6
+ errors?: IValidationError[];
6
7
  }
7
8
  export type ValidationMode = 'strict' | 'relaxed' | 'strip';
8
9
  export interface ValidationOptions {
9
10
  mode?: ValidationMode;
10
- tryConvert?: boolean;
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
- export declare function is<T>(input: unknown, options?: ValidationMode | ValidationOptions): input is ResolveDefaults<T>;
16
- export declare function assert<T>(input: unknown, options?: ValidationMode | ValidationOptions): ResolveDefaults<T>;
17
- export declare function assertGuard<T>(input: unknown, options?: ValidationMode | ValidationOptions): asserts input is ResolveDefaults<T>;
18
- export declare function validate<T>(input: unknown, options?: ValidationMode | ValidationOptions): IValidation<ResolveDefaults<T>>;
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';
package/dist/index.js CHANGED
@@ -1,3 +1,22 @@
1
+ const TRANSFORMER_MISSING = 'Typechecker transformer was not applied. Register { "transform": "@webergency-utils/typechecker/transformer" } in tsconfig plugins (requires ts-patch).';
2
+ /** Returns whether `input` already matches `T`. Does not coerce; `from` is ignored. */
3
+ export function is(_input, _options) {
4
+ throw new Error(TRANSFORMER_MISSING);
5
+ }
6
+ /** Validates and returns the (possibly coerced) value. Use `from` when conversion is needed. */
7
+ export function assert(_input, _options) {
8
+ throw new Error(TRANSFORMER_MISSING);
9
+ }
10
+ /** Asserts `input` already matches `T`. Does not coerce; `from` is ignored. */
11
+ export function assertGuard(_input, _options) {
12
+ throw new Error(TRANSFORMER_MISSING);
13
+ }
14
+ export function validate(_input, _options) {
15
+ throw new Error(TRANSFORMER_MISSING);
16
+ }
17
+ export function jsonSchema() {
18
+ throw new Error(TRANSFORMER_MISSING);
19
+ }
1
20
  export * from './runtime/validators.js';
2
21
  export * from './runtime/tags.js';
3
22
  export * from './runtime/casing.js';
@@ -1,5 +1,6 @@
1
1
  import type { Format } from './constraint.js';
2
2
  export type Email = Format<'email'>;
3
+ export type IdnEmail = Format<'idn-email'>;
3
4
  export type UUID = Format<'uuid'>;
4
5
  export type URL = Format<'url'>;
5
6
  export type IPv4 = Format<'ipv4'>;
@@ -10,6 +11,12 @@ export type Byte = Format<'byte'>;
10
11
  export type Password = Format<'password'>;
11
12
  export type Regex = Format<'regex'>;
12
13
  export type Hostname = Format<'hostname'>;
14
+ export type IdnHostname = Format<'idn-hostname'>;
15
+ export type URI = Format<'uri'>;
16
+ export type UriReference = Format<'uri-reference'>;
17
+ export type IRI = Format<'iri'>;
18
+ export type IriReference = Format<'iri-reference'>;
19
+ export type UriTemplate = Format<'uri-template'>;
13
20
  export type Time = Format<'time'>;
14
21
  export type Duration = Format<'duration'>;
15
22
  export type ObjectId = Format<'objectId'>;
@@ -1,3 +1,3 @@
1
- export type Default<V extends string | number | boolean | null> = {
1
+ export type Default<V = any> = {
2
2
  readonly __default?: V;
3
3
  };
@@ -47,7 +47,7 @@ type IsAny<T> = 0 extends 1 & T ? true : false;
47
47
  * Recursively resolves object properties, removing the optional `?` modifier
48
48
  * from any properties that have a `tag.Default` applied.
49
49
  */
50
- export type ResolveDefaults<T> = T extends Date | symbol | string | number | boolean | bigint | null | undefined | Function ? T : T extends Array<infer U> ? Array<ResolveDefaults<U>> : T extends object ? {
50
+ export type ResolveDefaults<T> = T extends Date | RegExp | Promise<any> | Map<any, any> | Set<any> | ArrayBuffer | SharedArrayBuffer | DataView | Buffer | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | BigInt64Array | BigUint64Array | symbol | string | number | boolean | bigint | null | undefined | Function ? T : T extends Array<infer U> ? Array<ResolveDefaults<U>> : T extends object ? {
51
51
  [K in keyof T as IsAny<T[K]> extends true ? never : '__default' extends keyof NonNullable<T[K]> ? K : never]-?: ResolveDefaults<Exclude<T[K], undefined>>;
52
52
  } & {
53
53
  [K in keyof T as IsAny<T[K]> extends true ? K : '__default' extends keyof NonNullable<T[K]> ? never : K]: ResolveDefaults<T[K]>;