js-checker
Version:
a type checker for javascript
371 lines (324 loc) • 11 kB
JavaScript
/**
* Created by zhangdianpeng on 2018/11/5.
*/
let _ = require('lodash');
let Err = require('./err');
let id = require('./id');
let render = require('./render/index.js');
let c = {};
let t = {};
let Any = message => id.judge(v => !_.isNil(v), message || 'is Null');
let Json = message => Any(message || 'is not an Json').judge(_.isObject, message || 'is not an Json');
let getOrValType = (arr, type, name = 'OrValType') => {
return c.OrVal.apply(null, arr.map(type)).match(value => type(value)).named(name).attributed({values: arr.map(type), type: type.show});
};
t.Null = id.judge(_.isNil, 'is not Null').named('Null');
t.Any = Any().named('Any');
t.Num = Any('is not a Num').judge(v => (_.isNumber(v) || _.isString(v)) && !isNaN(v) && _.trim(v) != '', 'is not a Num').match(Number).named('Num');
t.Str = Any('is not a Str').match(String).named('Str');
t.Fn = Any('is not a function').judge(_.isFunction, 'is not a function').named('Fn');
t.Json = Json().named('Json');
t.Obj = Json('is not an Obj').judge(v => !_.isArray(v), 'is not an Obj').named('TObj');
t.Arr = Json('is not an Arr').judge(_.isArray, 'is not an Arr').named('TArr');
t.Bool = Any('is not a Bool')
.match(value => {
let boolStr = value.toString();
if (boolStr === 'true') return true;
if (boolStr === 'false') return false;
})
.judge(_.isBoolean, 'is not a Bool')
.named('Bool');
t.Date = Any('is not a Date')
.match(value => new Date(value))
.judge(v => v.toString() != 'Invalid Date', 'is not a Date')
.named('Date');
t.Time = Any('is not a Time').match(String)
.judge(v => {
v = v.split(':');
if(v[0].length != 2 && v[0][0] != 0) return false;
if(v[1].length != 2 && v[1][0] != 0) return false;
return true;
}, 'is not a Time')
.named('Time');
c.Val = value => id
.judge(real => real == value, 'is not eq to ' + value)
.named(() => 'Value')
.attributed(value);
c.Or = function () {
let Types = [].slice.apply(arguments);
let newAttributes = [];
Types.forEach(T => {
let {name, attribute} = T.show;
if (name === 'Or') {
newAttributes = newAttributes.concat(attribute);
} else {
newAttributes.push(T.show);
}
});
return id
.match(value => {
let ret;
let es = [];
let matchSome = Types.some(Type => {
try {
ret = Type(value);
}
catch (e) {
es = es.concat(e.inspect().messages);
return false;
}
return true;
});
if (matchSome) return ret;
throw new Err(value, es);
})
.named('Or')
.attributed(newAttributes)
};
c.TagOr = function (Types, tags) {
if(!_.isArray(tags)){
tags = [tags];
}
let newAttributes = [];
let tagToType = {};
Types.forEach(T => {
let {name, attribute} = T.show;
if(name === 'Obj'){
newAttributes.push(T.show);
let tagIds = [];
tags.forEach(tag => {
if(attribute[tag] && attribute[tag].originType === 'c.Str'){
tagIds.push(attribute[tag].attribute.values[0]);
}else{
throw new Err(attribute, `these has a type does not satisfy the tag definition:${tags.join(',')}`)
}
});
if(tagToType[tagIds.join('-')]){
throw new Err(attribute, `Tag<${tags.join(',')}> repeat defined: ${tagIds.join(',')}`);
}
tagToType[tagIds.join('-')] = T;
}else{
throw new Err(attribute, `TagOr does not support ${name} type`);
}
});
return id
.match(value => {
let tagIds = [];
tags.forEach(tag => tagIds.push((value && value[tag])));
let type = tagToType[tagIds.join('-')];
if(!type){
throw new Err(value, `Tag definition<${tags.join(',')}> does not satisfy: ${Object.keys(tagToType).join('/')}`);
}
return type(value);
})
.named('Or')
.attributed(newAttributes);
};
c.Optional = Type => id
.match(value => c.Or(t.Null, Type)(value))
.named('Optional')
.attributed(Type.show);
c.Default = (Type, defaultValue) => id
.match(value => {
if (_.isNil(value)) return Type(defaultValue);
return Type(value);
})
.named('Default')
.attributed([Type.show, defaultValue]);
c.Arr = Type => t.Arr
.match(arr => arr.map((item, i) => {
let result;
try {
result = Type(item);
}
catch (e) {
let newErr = e.index(i);
newErr.value = arr;
throw newErr;
}
return result;
}))
.named('Arr')
.attributed(() => [Type.show]);
c.Obj = TypeMap => t.Obj
.match(obj => Object.keys(TypeMap).reduce((ret, key) => {
try {
let result = TypeMap[key](obj[key]);
if (!_.isNil(result))
ret[key] = result;
}
catch (e) {
let newErr = e.key(key);
newErr.value = obj;
throw newErr;
}
return ret;
}, {}))
.named('Obj')
.attributed(() => {
let result = {};
Object.keys(TypeMap).map(key => result[key] = TypeMap[key].show);
return result;
});
c.OrVal = function () {
let Types = [].slice.apply(arguments);
return c.Or.apply(null, Types.map(t => c.Val(t))).named('OrVal');
};
c.OrValType = (arr, type) => getOrValType(arr, type);
c.ValType = (value, type) => getOrValType([value], type);
c.Str = (value) => getOrValType([value], t.Str, 'Val').originTyped('c.Str');
c.Num = (value) => getOrValType([value], t.Num, 'Val');
c.OrStr = function(){
let params = Array.from(arguments);
return getOrValType(params, t.Str);
};
c.OrNum = function(){
let params = Array.from(arguments);
return getOrValType(params, t.Num);
};
c.ValSet = (setValue) => id.match(() => setValue).named('ValSet').attributed([setValue]);
c.ValConvert = (before, after) => id
.match(value => {
value = c.Val(before)(value);
return c.ValSet(after)(value);
})
.named('ValConvert')
.attributed([before, after]);
c.ObjConvert = (objType, beforeKey, afterKey) => id
.match(obj => {
let newObj = c.Obj(objType)(obj);
newObj[afterKey] = newObj[beforeKey];
delete newObj[beforeKey];
return newObj;
})
.named('ObjConvert')
.attributed([objType.show, beforeKey, afterKey]);
c.Map = (KeyType, ValueType) => t
.Obj
.match(obj => Object.keys(obj).reduce((ret, key) => {
let newKey, newValue;
try {
newKey = KeyType(key);
} catch (e) {
throw e.key(`the key<${key}> in Map`);
}
try {
newValue = ValueType(obj[key]);
} catch (e) {
throw e.key(`the value<${obj[key]}> of key<${key}> in Map `);
}
ret[newKey] = newValue;
return ret;
}, {}))
.named('Map')
.attributed([KeyType.show, ValueType.show]);
c.Custom = (fn, options = {}) => {
let attribute, name, realType, params = [];
try{
realType = fn.apply(null, params);
attribute = realType.show.attribute;
name = realType.show.name;
}catch(err){
attribute = options.attribute || {};
name = 'Custom';
realType = undefined;
}
return id
.match(value => {
if(realType){
return realType(value);
}else{
return fn.apply(null, params)(value)
}
})
.named(name)
.attributed(attribute);
};
c.Fn = ({input = [], output = t.Any}) => {
return id
.match(fn => {
return function(){
let params = Array.from(arguments);
let newParams = input.map((type, index) => {
let res;
try{
res = type(params[index]);
}catch(err){
throw err.key(`The ${index}th parameter of the function`);
}
return res;
});
let ouputResult = fn.apply(null ,newParams);
try{
ouputResult = output(ouputResult);
}catch(err){
throw err.key(`the output of the function`);
}
return ouputResult;
};
})
.named('CFn')
.attributed({
input: input.map(t => t.show),
output: output.show
});
};
c.Extend = function () {
let Types = Array.prototype.slice.call(arguments);
let attributes = [];
let newName = 'Obj';
Types.forEach(t => {
let {attribute, name} = t.show;
if(name === 'Obj'){
if(attributes.length){
attributes = attributes.map(a => Object.assign({}, a, attribute));
}else{
attributes.push(attribute);
}
}else if(name === 'Or'){
newName = 'Or';
if(attributes.length){
attributes = attributes.reduce((all, a) => all.concat(attribute.map(b => Object.assign({}, a, b.attribute))), []);
}else{
attributes = attribute.map(b => b.attribute);
}
}else{
throw new Err(attribute, `Extend does not support ${name} type`);
}
});
let newAttribute = newName === 'Obj' ?
attributes[0]:
attributes.map(a => ({name: 'Obj', attribute: a}));
return id
.match(value => {
let values = Types.map(Type => Type(value));
return Object.assign.apply(Object, values);
})
.named(newName)
.attributed(newAttribute);
};
['Null', 'Any', 'Num', 'Str', 'Fn', 'Json', 'Obj', 'Arr', 'Bool', 'Date', 'Time']
.forEach(fnName => {
t['D' + fnName] = (des) => {
let type = t[fnName];
return id
.match(type)
.named(type.show.name)
.attributed(type.show.attribute)
.described(des);
};
});
['Val', 'Or', 'Optional', 'Default', 'Arr', 'Obj', 'OrVal', 'OrValType', 'ValType', 'ValSet', 'ValConvert', 'ObjConvert', 'Map', 'Extend', 'Fn', 'Custom', 'TagOr', 'Str', 'Num', 'OrNum', 'OrStr']
.forEach(fnName => c['D' + fnName] = (des) => function(){
let args = Array.prototype.slice.call(arguments);
let type = c[fnName].apply(null, args);
return id
.match(type)
.named(type.show.name)
.attributed(type.show.attribute)
.described(des);
});
module.exports = Object.assign({
c,
t
}, render);