-
Notifications
You must be signed in to change notification settings - Fork 0
/
typed.js
202 lines (183 loc) · 7.51 KB
/
typed.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
'use strict';
const getFunctionArgumentsDescribe = (func) => {
/*
snatch from http://stackoverflow.com/a/31194949
*/
return (Function.prototype.toString.call(func) + '').replace(/\s+/g, '')
.replace(/[/][*][^/*]*[*][/]/g, '') // strip simple comments
.split('){', 1)[0].replace(/^[^(]*[(]/, '') // extract the parameters
.replace(/{+/g, '') // strip any ES6 defaults
.replace(/}+/g, '') // strip any ES6 defaults
.replace(/=[^,]+/g, '') // strip any ES6 defaults
.split(',').filter(Boolean); // split & filter [""]
};
const defaultTypes = {
'str': (value) => {
const typeOfValue = {}.toString.call(value).slice(8, -1);
if (typeOfValue !== 'String') {
throw new TypeError(`str parameter must be String instance, not ${typeOfValue}`);
}
},
'uInt': (value) => {
const typeOfValue = {}.toString.call(value).slice(8, -1);
if (typeOfValue !== 'Number') {
throw new TypeError(`uInt parameter must be Number instance, not ${typeOfValue}`);
}
if (!Number.isSafeInteger(value)) {
throw new TypeError(`uInt parameter must be safe integer value`);
}
if (value < 0) {
throw new TypeError(`uInt parameter must be more than 0, not ${value}`);
}
}
};
const typeTesterDefault = (types, functionName, ...passedArgs) => {
//compare length of function passed args and length of expected args
const argsLength = passedArgs.length;
const typesOfParams = getFunctionArgumentsDescribe(functionName);
if (argsLength !== typesOfParams.length) {
throw new RangeError('amount of parameters is invalid');
} else {
const allTypes = Object.keys(types);
const allTypesLength = allTypes.length;
for (let i = 0; i < argsLength; i++) {
let typeDefinedInTypesObject = false;
for (let j = 0; j < allTypesLength; j++) {
if (typesOfParams[i].startsWith(allTypes[j])) {
typeDefinedInTypesObject = true;
types[allTypes[j]](passedArgs[i]);
break;
}
}
if (!typeDefinedInTypesObject) {
throw new RangeError(`can not find type of ${typesOfParams[i]} value in [${allTypes}]`);
}
}
return true;
}
};
const mapOfAllOwnSetters = (obj) => {
/*
get prototype chain
*/
const mapOfSetters = new Map();
const excludeConstructors = [
({}).constructor,
(Number()).constructor,
(String()).constructor,
(new Function()).constructor
];
const protoChain = function protoChain(obj){
const proto = Object.getPrototypeOf(obj);
Object.getOwnPropertyNames(obj).forEach((property) => {
const descOfProp = Object.getOwnPropertyDescriptor(obj, property);
if (descOfProp.set && descOfProp.configurable && (!descOfProp.value)) {
if(!mapOfSetters.has(property)){
mapOfSetters.set(property, descOfProp.set);
}
}
});
if(!excludeConstructors.includes(proto.constructor)){
protoChain(proto);
}
};
protoChain(obj);
return mapOfSetters;
};
const arrOfAllOwnWritableAndConfigurableMethods = (obj) => {
return Object.getOwnPropertyNames(obj).filter((property) => {
const descOfProp = Object.getOwnPropertyDescriptor(obj, property);
return (descOfProp.value instanceof Function) && descOfProp.writable && descOfProp.configurable;
});
};
class TypedProxy {
constructor(typedClass, types = defaultTypes, typeTester = typeTesterDefault) {
/*
Work with Class only
*/
if (!(typedClass instanceof Function)) {
throw new TypeError('typedClass must be a Function instance');
}
/*
Determinate static methods
*/
const createApplyProxy = (instance, methodName) => {
const handler = {
apply(target, thisArgument, argList){
typeTester(types, target, ...argList);
return target.apply(thisArgument, argList);
}
};
return new Proxy(instance[methodName], handler);
};
const mapApplyHandler = (instance, methodNames, ...excludeMethods) => {
const setOfStaticMethodsAndThemProxies = new Map();
methodNames.forEach((methodName) => {
if (!excludeMethods.includes(methodName)) {
setOfStaticMethodsAndThemProxies.set(methodName, createApplyProxy(instance, methodName));
}
});
return setOfStaticMethodsAndThemProxies;
};
const staticMethodOfTypedClass = arrOfAllOwnWritableAndConfigurableMethods(typedClass);
const setOfStaticMethodsAndThemProxies = mapApplyHandler(typedClass, staticMethodOfTypedClass);
const mapOfStaticSetter = mapOfAllOwnSetters(typedClass);
const handler = {
construct(target, argList) {
/*
Interception for 'new' statement
Here we checks argList with typeTester
*/
typeTester(types, typedClass, ...argList);
const instance = new target(...argList);
const methodsOfTypeInstance = arrOfAllOwnWritableAndConfigurableMethods(Object.getPrototypeOf(instance));
const mapSettersOfInstance = mapOfAllOwnSetters(instance);
const excludeMethods = ['constructor'];
const mapOfMethodsAndThemProxies = mapApplyHandler(instance, methodsOfTypeInstance, ...excludeMethods);
const instanceHandler = {
get(target, prop){
if(!excludeMethods.includes(prop)){
if (!mapOfMethodsAndThemProxies.has(prop)) {
if(target[prop] instanceof Function){
mapOfMethodsAndThemProxies.set(prop, createApplyProxy(target, prop));
} else {
return target[prop];
}
}
return mapOfMethodsAndThemProxies.get(prop);
} else {
return target[prop];
}
},
set(target, prop, value){
if (mapSettersOfInstance.has(prop)) {
typeTester(types, mapSettersOfInstance.get(prop), value);
}
target[prop] = value;
return true;
}
};
return new Proxy(instance, instanceHandler);
},
get(target, prop){
/*
static methods
*/
if (setOfStaticMethodsAndThemProxies.has(prop)) {
return setOfStaticMethodsAndThemProxies.get(prop);
} else {
return target[prop];
}
},
set(target, prop, value){
if (mapOfStaticSetter.has(prop)) {
typeTester(types, mapOfStaticSetter.get(prop), value);
}
target[prop] = value;
return true;
}
};
return new Proxy(typedClass, handler);
}
}
module.exports = TypedProxy;