skemata
Version:
An object structure and type validation library
275 lines (248 loc) • 7.89 kB
JavaScript
;
const bestSuggestions = require('./suggest');
const util = require('./util');
const _type = util._type;
const _clone = util._clone;
const v = {};
const _result = (ok, expType, acType, val, warning) => {
if (ok) {
return {ok: true, warning};
}
return {ok: false, type: 'simple', expType, acType, val};
};
const _collResult = (itemResults, warning) => {
const hasErrItems = itemResults.filter(item => {
const result = item.result;
return !result.ok;
}).length > 0;
if (hasErrItems) {
return {ok: false, type: 'collection', itemResults, warning};
} else {
return {ok: true, type: 'collection', itemResults, warning};
}
};
const _collItem = (path, result) => {
return {path, result};
};
const defType = (human, fn, _props, defaultValue) => {
fn.human = human;
fn._props = _props;
if (defaultValue !== undefined) fn.defaultValue = defaultValue;
fn.default = val => {
const newFn = fn.bind({});
return defType(human, newFn, _props, val);
};
fn.undefault = () => {
const newFn = fn.bind({});
return defType(human, newFn, _props);
};
return fn;
};
const isType = type => {
return defType(type, x => {
if (_type(x) === type) {
return _result(true);
}
return _result(false, type, _type(x), x);
});
};
const compose = (f1, f2) => {
return defType(f1.human, x => {
const r = f1(x);
if (!r.ok) return r;
return f2(x);
}, f2._props);
};
const _arrayOf = (elType) => {
return ary => {
const rs = ary.map(el => elType(el));
const rs_ = rs.map((r, idx) => {
return _collItem(idx, r);
});
return _collResult(rs_);
};
};
const _objectOf = (schema, suggest) => {
if (suggest === undefined) suggest = true;
const fn = obj => {
const rs = Object.keys(obj).map(key => {
const val = obj[key];
if (!(key in schema)) {
const r = _result(true);
let warning;
if (suggest) {
const suggestions = bestSuggestions(key, Object.keys(schema));
if (suggestions.length > 0) {
warning = `perhaps you meant ${suggestions.join(', ')}`;
}
}
if (warning) {
r.warning = warning;
}
return _collItem(key, r);
}
const ty = schema[key];
const r = ty(val);
return _collItem(key, r);
});
Object.keys(schema).forEach(key => {
if (key in obj) {
return;
}
const ty = schema[key];
if ('defaultValue' in ty) {
obj[key] = _clone(ty.defaultValue);
ty(obj[key]);
}
});
return _collResult(rs);
};
fn._props = {schema, suggest};
return fn;
};
const _mergeObjectOf = (v1, v2, opts) => {
const deepUndefault = val => {
if (val.human === 'object' && val._props && val._props.elSchema && val._props.elSchema.human === 'object') {
const blank = v.objects({}, v.object({}));
return _mergeObjectOf(val, blank, opts);
} else if (val.human === 'object' && val._props && val._props.schema) {
const blank = v.object({});
return _mergeObjectOf(val, blank, opts);
} else {
return val.undefault();
}
};
const uniq = ary => Array.from(new Set(ary));
if (v1.human === 'object' && v2.human === 'object' && v1._props.schema && v2._props.schema) {
const schema1 = v1._props.schema;
const schema2 = v2._props.schema;
const mergedSchema = {};
const allKeys = uniq(Object.keys(schema1).concat(Object.keys(schema2)));
allKeys.forEach(key => {
if (key in schema1 && key in schema2) {
// present in both
const val1 = schema1[key];
const val2 = schema2[key];
try {
const val = _mergeObjectOf(val1, val2, opts);
mergedSchema[key] = val;
} catch (e) {
if (val1.human === val2.human) {
const val = val2;
mergedSchema[key] = val;
} else {
throw new Error(`can't merge schemas for key '${key}' (${val1.human} and ${val2.human})`);
}
}
if ('defaultValue' in val2) {
mergedSchema[key] = mergedSchema[key].default(val2.defaultValue);
} else if ('defaultValue' in val1) {
mergedSchema[key] = mergedSchema[key].default(val1.defaultValue);
}
} else {
let val = key in schema1 ? schema1[key] : schema2[key];
if (opts && opts.ignoreFirstDefaults && key in schema1) {
val = deepUndefault(val);
}
mergedSchema[key] = val;
}
});
return v.object(mergedSchema);
} else if (v1.human === 'object' && v2.human === 'object' && v1._props.elSchema && v2._props.elSchema) {
const elSchema1 = v1._props.elSchema;
const elSchema2 = v2._props.elSchema;
const mergedElSchema = _mergeObjectOf(elSchema1, elSchema2, opts);
return v.objects(v1._props.props, mergedElSchema);
}
throw new Error("can't merge");
};
const _objectsOf = (props, elSchema) => {
const specifics = props && props.specifics || {};
const fn = obj => {
const rs = Object.keys(obj).map(key => {
const val = obj[key];
let warning;
if (props && props.keys && props.keys.indexOf(key) === -1) {
const customWarning = props.warner && props.warner(key);
if (customWarning) {
warning = customWarning;
} else {
const suggestions = bestSuggestions(key, props.keys);
warning = `unrecognized key: ${key}; expected either of ${props.keys.join(', ')}`;
if (suggestions.length > 0) {
warning = warning + '; perhaps you meant ' + suggestions.join(', ');
}
}
}
const itemSchema = key in specifics ? specifics[key] : elSchema;
const r = itemSchema(val);
if (warning) {
r.warning = r.warning ? r.warning + '; ' + warning : warning;
}
return _collItem(key, r);
});
Object.keys(specifics).forEach(key => {
if (key in obj) return;
const ty = specifics[key];
if ('defaultValue' in ty) {
obj[key] = _clone(ty.defaultValue);
ty(obj[key]);
}
});
return _collResult(rs);
};
fn._props = {props, elSchema};
return fn;
};
v.noop = () => _result(true);
v._string = isType('string');
v._bool = isType('boolean');
v._array = isType('array');
v._function = isType('function');
v._regexp = isType('regexp');
v._number = isType('number');
v._object = isType('object');
v.string = v._string;
v.bool = v._bool;
v.function = v._function;
v.regexp = v._regexp;
v.int = v._number;
v.int.human = 'int';
v.array = elType => compose(v._array, _arrayOf(elType));
v.object = (schema, suggest) => compose(v._object, _objectOf(schema, suggest));
v.objects = (props, elSchema) => compose(v._object, _objectsOf(props, elSchema));
v.merge = _mergeObjectOf;
v.either = function() {
const types = [].slice.call(arguments);
const humanTypes = types.map(type => type.human || '<>');
const human = `either of types ${JSON.stringify(humanTypes)}`;
return defType(human, x => {
const type = types.find(t => t(x).ok);
if (type) {
return type(x);
}
return _result(false, human, _type(x), x);
});
};
v.enum = function() {
const vals = [].slice.call(arguments);
const human = `either of values ${JSON.stringify(vals)}`;
return defType(human, x => {
const check = vals.indexOf(x) !== -1;
if (check) {
return _result(true);
}
return _result(false, human, _type(x), x);
});
};
v.deprecated = (elType, message) => {
return defType(elType.human, x => {
const r = elType(x);
const warning = 'deprecated' + (message ? ': ' + message : '');
r.warning = r.warning ? r.warning + '; ' + warning : warning;
return r;
});
};
v.anymatch_ = v.either(v.string, v.regexp, v.function);
v.anymatch = v.either(v.anymatch_, v.array(v.anymatch_));
module.exports = v;