Skip to content

Commit cb1a4dd

Browse files
authored
Add way to declare custom/project-based type validations (#66)
* add custom validations * refactor out customTypes into customTypes.mjs, remove PC specific code * Asserter: add @ignoreRTI tag for skipping runtype type assertion generation * add two tests
1 parent da93e3c commit cb1a4dd

File tree

8 files changed

+132
-78
lines changed

8 files changed

+132
-78
lines changed

src-runtime/customTypes.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* @type {Record<string, (value: any) => boolean>}
3+
*/
4+
const customTypes = {};
5+
export {customTypes};

src-runtime/customValidations.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/**
2+
* @type {Function[]}
3+
*/
4+
const customValidations = [];
5+
export {customValidations};

src-runtime/index.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export * from './assertType.mjs';
22
export * from './createDiv.mjs';
3+
export * from './customTypes.mjs';
4+
export * from './customValidations.mjs';
35
export * from './makeJSDoc.mjs';
46
export * from './registerClass.mjs';
57
export * from './registerTypedef.mjs';

src-runtime/typecheckOptions.mjs

Lines changed: 0 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -11,38 +11,6 @@ const typecheckOptions = {
1111
*/
1212
warned: {},
1313
logSuperfluousProperty: false,
14-
/**
15-
* @todo integrate PC/Transformers Examples browser + custom config for each project
16-
*/
17-
customTypes: {
18-
AnimSetter(value) {
19-
//console.log("@todo Is AnimSetter?", value);
20-
return true;
21-
},
22-
AnimBinder(value) {
23-
//console.log("@todo Is AnimBinder?", value);
24-
return true;
25-
},
26-
AnimCurvePath(value) {
27-
//console.log("@todo Is AnimCurvePath?", value);
28-
return true;
29-
},
30-
ComponentData(value) {
31-
//console.log("@todo Is ComponentData?", value);
32-
return true;
33-
},
34-
Renderer(value) {
35-
// E.g. instance of `ForwardRenderer`
36-
//debugger;
37-
return value?.constructor?.name?.endsWith("Renderer");
38-
},
39-
IArguments(value) {
40-
//console.log("@todo Is IArguments?", value);
41-
return true;
42-
},
43-
// @todo validate textures having width != 0 and height != 0
44-
// @todo validate integers
45-
},
4614
count: 0,
4715
};
4816
function typecheckReport() {

src-runtime/validateType.mjs

Lines changed: 30 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
import {classes } from "./registerClass.mjs";
2-
import {typedefs } from "./registerTypedef.mjs";
3-
import {typecheckOptions} from "./typecheckOptions.mjs";
4-
import {typecheckWarn } from "./typecheckWarn.mjs";
5-
import {validateArray } from "./validateArray.mjs";
6-
import {validateMap } from "./validateMap.mjs";
7-
import {validateNumber } from "./validateNumber.mjs";
8-
import {validateObject } from "./validateObject.mjs";
9-
import {validateRecord } from "./validateRecord.mjs";
10-
import {validateSet } from "./validateSet.mjs";
11-
import {validateTuple } from "./validateTuple.mjs";
12-
import {validateTypedef } from "./validateTypedef.mjs";
13-
import {validateUnion } from "./validateUnion.mjs";
14-
// For quickly checking props of Vec2/Vec3/Vec4/Quat/Mat3/Mat4 without GC
15-
const propsXY = ['x', 'y'];
16-
const propsXYZ = ['x', 'y', 'z'];
17-
const propsXYZW = ['x', 'y', 'z', 'w'];
18-
const props9 = [0, 1, 2, 3, 4, 5, 6, 7, 8];
19-
const props16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
1+
import {customTypes } from "./customTypes.mjs";
2+
import {customValidations} from "./customValidations.mjs";
3+
import {classes } from "./registerClass.mjs";
4+
import {typedefs } from "./registerTypedef.mjs";
5+
import {typecheckWarn } from "./typecheckWarn.mjs";
6+
import {validateArray } from "./validateArray.mjs";
7+
import {validateMap } from "./validateMap.mjs";
8+
import {validateNumber } from "./validateNumber.mjs";
9+
import {validateObject } from "./validateObject.mjs";
10+
import {validateRecord } from "./validateRecord.mjs";
11+
import {validateSet } from "./validateSet.mjs";
12+
import {validateTuple } from "./validateTuple.mjs";
13+
import {validateTypedef } from "./validateTypedef.mjs";
14+
import {validateUnion } from "./validateUnion.mjs";
15+
let enabled = true;
16+
export function disableTypeChecking() {
17+
enabled = false;
18+
}
19+
export function enableTypeChecking() {
20+
enabled = true;
21+
}
2022
/**
2123
* @param {*} value - The actual value which we need to check.
2224
* @param {object} expect - Can also be a string, but string|object is unsupported in VSCode
@@ -36,6 +38,9 @@ const props16 = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
3638
* @returns {boolean} Returns wether `value` is in the shape of `expect`.
3739
*/
3840
function validateType(value, expect, loc, name, critical = true) {
41+
if (!enabled) {
42+
return true;
43+
}
3944
if (typeof expect === 'string') {
4045
expect = {
4146
type: expect,
@@ -59,7 +64,7 @@ function validateType(value, expect, loc, name, critical = true) {
5964
return true;
6065
}
6166
}
62-
const customCheck = typecheckOptions.customTypes[type];
67+
const customCheck = customTypes[type];
6368
if (customCheck) {
6469
return customCheck(value);
6570
}
@@ -76,34 +81,13 @@ function validateType(value, expect, loc, name, critical = true) {
7681
return false;
7782
}
7883
}
79-
if (typeof window !== 'undefined' && window.pc) {
80-
const {pc} = window;
81-
/**
82-
* @param {string|number} prop - Something like 'x', 'y', 'z', 'w', 0, 1, 2, 3, 4 etc.
83-
* @returns {boolean} Wether prop is a valid number.
84-
*/
85-
const checkProp = (prop) => {
86-
return validateNumber(value, prop);
87-
};
88-
// In a bundle pc can be defined while pc is not filled with all classes yet,
89-
// therefore we need to check if class is added already.
90-
if (pc.Vec2 && value instanceof pc.Vec2) {
91-
return propsXY.every(checkProp);
92-
}
93-
if (pc.Vec3 && value instanceof pc.Vec3) {
94-
return propsXYZ.every(checkProp);
95-
}
96-
if ((pc.Vec4 && value instanceof pc.Vec4) || (pc.Quat && value instanceof pc.Quat)) {
97-
return propsXYZW.every(checkProp);
98-
}
99-
if (pc.Mat3 && value instanceof pc.Mat3) {
100-
return props9.every(prop => validateNumber(value.data, prop));
101-
}
102-
if (pc.Mat4 && value instanceof pc.Mat4) {
103-
return props16.every(prop => validateNumber(value.data, prop));
84+
for (const customValidation of customValidations) {
85+
const ret = customValidation(value, expect, loc, name, critical);
86+
if (!ret) {
87+
typecheckWarn(`${loc}> customValidation failed`);
88+
return false;
10489
}
10590
}
106-
// todo: either switch or object lookup for custom hooks
10791
if (type === "object") {
10892
return validateObject(value, properties, loc, name, critical);
10993
}

src-transpiler/Asserter.mjs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,9 @@ class Asserter extends Stringifier {
223223
[paramName]: parseJSDocSetter(lastComment.value, this.expandType)
224224
};
225225
}
226+
if (lastComment.value.includes('@ignoreRTI')) {
227+
return;
228+
}
226229
return parseJSDoc(lastComment.value, this.expandType);
227230
}
228231
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {customTypes, disableTypeChecking, enableTypeChecking} from '@runtime-type-inspector/runtime';
2+
/**
3+
* @ignoreRTI
4+
* @param {*} value - The value.
5+
* @returns {boolean} Whether "value" if of type "expect".
6+
*/
7+
function validatePrime(value) {
8+
if (typeof value !== 'number') {
9+
return false;
10+
}
11+
// Preventation for: Uncaught RangeError: Maximum call stack size exceeded
12+
disableTypeChecking();
13+
const ret = isPrime(value);
14+
enableTypeChecking();
15+
return ret;
16+
}
17+
customTypes.prime = validatePrime;
18+
/**
19+
* @typedef {number} prime
20+
*/
21+
/**
22+
* @param {prime} a - 1st prime addend.
23+
* @param {prime} b - 2nd prime addend.
24+
* @returns {number} The sum.
25+
*/
26+
function addPrimesOnly(a, b) {
27+
return a + b;
28+
}
29+
/**
30+
* @param {number} x - The number to test.
31+
* @returns {boolean} Whether number is a prime.
32+
*/
33+
function isPrime(x) {
34+
if (x > 2 && x % 2 === 0) {
35+
return false;
36+
}
37+
const n = Math.ceil(Math.sqrt(x));
38+
for (let i = 3; i <= n; i += 2) {
39+
if (x % i === 0) {
40+
return false;
41+
}
42+
}
43+
return x > 1;
44+
}
45+
// Tests:
46+
addPrimesOnly(11, 13); // works, both prime
47+
addPrimesOnly(1, 13); // fails, 1 isn't prime
48+
addPrimesOnly(11, undefined); // not even a number
49+
const primes = [];
50+
for (let i = 0; i < 200; i++) {
51+
const prime = isPrime(i);
52+
if (prime) {
53+
primes.push(i);
54+
}
55+
}
56+
console.log("Primes:", primes);
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import {customValidations, typecheckWarn} from '@runtime-type-inspector/runtime';
2+
class Vec3 {
3+
constructor(x = 0, y = 0, z = 0) {
4+
this.x = x;
5+
this.y = y;
6+
this.z = z;
7+
}
8+
}
9+
class Entity {
10+
/**
11+
* @param {string} [name] - The name.
12+
*/
13+
constructor(name = "Untitled") {
14+
this.name = name;
15+
}
16+
}
17+
console.log("customValidations", customValidations);
18+
customValidations.length = 0;
19+
customValidations.push(value => {
20+
if (value instanceof Vec3) {
21+
if (isNaN(value.x)) {
22+
typecheckWarn("x is NaN");
23+
return false;
24+
}
25+
}
26+
return true;
27+
});
28+
const entity1 = new Entity("test");
29+
console.log("entity1", entity1);
30+
const entity2 = new Entity(new Vec3(NaN, 2, 3));
31+
console.log("entity2", entity2);

0 commit comments

Comments
 (0)