specified
Version:
Type-safe typescript data specification verification
382 lines (381 loc) • 15.7 kB
JavaScript
"use strict";
var __assign = (this && this.__assign) || function () {
__assign = Object.assign || function(t) {
for (var s, i = 1, n = arguments.length; i < n; i++) {
s = arguments[i];
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
t[p] = s[p];
}
return t;
};
return __assign.apply(this, arguments);
};
Object.defineProperty(exports, "__esModule", { value: true });
var objectDefinition = function (schema, typeName) { return ({
type: typeName,
nested: Object.keys(schema).reduce(function (o, a) {
o[a] = schema[a].definition;
return o;
}, {}),
descriptions: Object.keys(schema).reduce(function (d, a) {
if (schema[a].hasOwnProperty("description")) {
d[a] = schema[a].description;
}
return d;
}, {})
}); };
var objectEval = function (schema, defaultStrict, typeName) { return function (data, options) {
var settings = __assign({ strict: defaultStrict, failEarly: false }, options.global, options.local);
if (typeof data !== "object" || data === null || data instanceof Array) {
return { err: { code: "type." + typeName + ".not_a_regular_object", value: data, message: "Not a regular object." } };
}
var nestedErrors = [];
if (settings.strict) {
var extraKeys = Object.keys(data).filter(function (k) { return !(k in schema); });
extraKeys.forEach(function (ek) {
nestedErrors.push({ code: "type." + typeName + ".extra_attribute", key: ek, value: data[ek], message: "Data has attribute that is not part of the strict schema: \"" + ek + "\"." });
});
}
var attributes = Object.keys(schema);
var model = {};
for (var attrIndex = 0; attrIndex < attributes.length && (!settings.failEarly || !nestedErrors.length); ++attrIndex) {
var attr = attributes[attrIndex];
var attrSpec = schema[attr];
if (!(attr in data) && !attrSpec.optional && !attrSpec.hasOwnProperty("defaultValue")) {
nestedErrors.push({ code: "type." + typeName + ".missing_attribute", value: data, message: "Missing attribute: \"" + attr + "\".", key: attr });
}
else {
var pass = Symbol();
var value = data.hasOwnProperty(attr) ? data[attr] : attrSpec.optional ? pass : attrSpec.hasOwnProperty("defaultValue") ? attrSpec.defaultValue : undefined;
if (value !== pass) {
var attrResult = attrSpec.eval(value, { global: options.global });
if (attrResult.err) {
nestedErrors.push({ code: "type." + typeName + ".invalid_attribute", value: value, message: "Evaluation of attribute \"" + attr + "\" failed.", key: attr, nestedErrors: [attrResult.err] });
}
else {
model[attr] = attrResult.value;
}
}
}
}
if (nestedErrors.length) {
return { err: { code: "type." + typeName + ".invalid_attribute_data", value: data, message: "Invalid attribute data.", nestedErrors: nestedErrors } };
}
return { err: null, value: model };
}; };
exports.Type = {
unknown: {
version: 1,
definition: {
type: "unknown"
},
eval: function (value) {
return { err: null, value: value };
}
},
null: {
version: 1,
definition: {
type: "null"
},
eval: function (value) {
if (value !== null) {
return { err: { code: "type.null.not_null", value: value, allowed: null, message: "Not null." } };
}
return { err: null, value: null };
}
},
string: {
version: 1,
definition: {
type: "string"
},
eval: function (value) {
if (typeof value !== "string") {
return { err: { code: "type.string.not_a_string", value: value, message: "Not a string." } };
}
return { err: null, value: value };
}
},
number: {
version: 1,
definition: {
type: "number"
},
eval: function (value) {
if (typeof value !== "number") {
return { err: { code: "type.number.not_a_number", value: value, message: "Not a number." } };
}
return { err: null, value: value };
}
},
boolean: {
version: 1,
definition: {
type: "boolean"
},
eval: function (value) {
if (typeof value !== "boolean") {
return { err: { code: "type.boolean.not_a_boolean", value: value, message: "Not a boolean." } };
}
return { err: null, value: value };
}
},
symbol: {
version: 1,
definition: {
type: "symbol"
},
eval: function (value) {
if (typeof value !== "symbol") {
return { err: { code: "type.symbol.not_a_symbol", value: value, message: "Not a symbol." } };
}
return { err: null, value: value };
}
},
literal: function (def) { return ({
version: 1,
definition: {
type: "literal",
settings: { values: Object.keys(def) }
},
eval: function (value) {
if (typeof value !== "string" || !def.hasOwnProperty(value)) {
return { err: { code: "type.literal.incorrect_literal", value: value, allowed: Object.keys(def), message: "Incorrect literal." } };
}
return { err: null, value: value };
}
}); },
literalValue: function () {
var values = [];
for (var _i = 0; _i < arguments.length; _i++) {
values[_i] = arguments[_i];
}
return ({
version: 1,
definition: {
type: "literalValue",
settings: { values: values }
},
eval: function (value) {
for (var _i = 0, values_1 = values; _i < values_1.length; _i++) {
var v = values_1[_i];
if (value === v) {
return { err: null, value: value };
}
}
return { err: { code: "type.literalValue.incorrect_literal_value", value: value, allowed: values, message: "Incorrect literal value." } };
}
});
},
array: function (spec) { return ({
version: 1,
definition: {
type: "array",
nested: { element: spec.definition }
},
eval: function (data, options) {
var settings = __assign({ failEarly: false, skipInvalid: false }, options.global, options.local);
if (!(data instanceof Array)) {
return { err: { code: "type.array.not_an_array", value: data, message: "Not an array." } };
}
var values = [];
var nestedErrors = [];
for (var i = 0; i < data.length && (!settings.failEarly || !nestedErrors.length); ++i) {
var elementValue = data[i];
var elementResult = spec.eval(elementValue, { global: options.global });
if (elementResult.err) {
if (!settings.skipInvalid) {
nestedErrors.push({
code: "type.array.invalid_element",
value: elementValue,
message: "Evaluation of array element at index \"" + i + "\" failed.",
key: i,
nestedErrors: [elementResult.err]
});
}
}
else {
values.push(elementResult.value);
}
}
if (nestedErrors.length) {
return { err: { code: "type.array.invalid_elements", value: data, message: "Array validation failed.", nestedErrors: nestedErrors } };
}
return { err: null, value: values };
}
}); },
object: function (schema) {
return {
version: 1,
definition: objectDefinition(schema, "object"),
eval: objectEval(schema, true, "object")
};
},
interface: function (schema) {
return {
version: 1,
definition: objectDefinition(schema, "interface"),
eval: objectEval(schema, false, "interface")
};
},
map: function (keySpec, valueSpec) {
return {
version: 1,
definition: {
type: "map",
nested: {
key: keySpec.definition,
value: valueSpec.definition
}
},
eval: function (data, options) {
var settings = __assign({ failEarly: false, skipInvalidKeys: false, skipInvalidValues: false }, options.global, options.local);
if (typeof data !== "object" || data === null || data instanceof Array) {
return { err: { code: "type.map.not_a_regular_object", value: data, message: "Not a regular object." } };
}
var result = {};
var nestedErrors = [];
var dataKeys = Object.keys(data);
for (var dataKeyIndex = 0; dataKeyIndex < dataKeys.length && (!settings.failEarly || !nestedErrors.length); ++dataKeyIndex) {
var dk = dataKeys[dataKeyIndex];
var keyResult = keySpec.eval(dk, { global: options.global });
if (keyResult.err) {
if (!settings.skipInvalidKeys) {
nestedErrors.push({ code: "type.map.invalid_key", value: dk, message: "Evaluation of map key \"" + dk + "\" failed.", key: dk, nestedErrors: [keyResult.err] });
}
}
else {
var value = data[dk];
var valueResult = valueSpec.eval(value, { global: options.global });
if (valueResult.err) {
if (!settings.skipInvalidValues) {
nestedErrors.push({ code: "type.map.invalid_value", value: value, message: "Evaluation of map value for key \"" + dk + "\" failed.", key: dk, nestedErrors: [valueResult.err] });
}
}
else {
result[keyResult.value] = valueResult.value;
}
}
}
if (nestedErrors.length) {
return { err: { code: "type.map.invalid_data", value: data, message: "Invalid map data.", nestedErrors: nestedErrors } };
}
return { err: null, value: result };
}
};
},
tuple: function () {
var specs = [];
for (var _i = 0; _i < arguments.length; _i++) {
specs[_i] = arguments[_i];
}
return {
version: 1,
definition: {
type: "tuple",
nested: specs.reduce(function (n, spec, i) {
n[i] = spec.definition;
return n;
}, {})
},
eval: function (data, options) {
var settings = __assign({ failEarly: false }, options.global, options.local);
if (!(data instanceof Array)) {
return { err: { code: "type.tuple.not_a_tuple", value: data, message: "Not an tuple." } };
}
if (data.length !== specs.length) {
return { err: { code: "type.tuple.incorrect_length", value: data, message: "Data does not have correct tuple length " + specs.length + "." } };
}
var values = [];
var nestedErrors = [];
for (var i = 0; i < data.length && (!settings.failEarly || !nestedErrors.length); ++i) {
var elementValue = data[i];
var elementResult = specs[i].eval(elementValue, { global: options.global });
if (elementResult.err) {
nestedErrors.push({
code: "type.tuple.invalid_element",
value: elementValue,
message: "Evaluation of tuple element at index \"" + i + "\" failed.",
key: i,
nestedErrors: [elementResult.err]
});
}
else {
values.push(elementResult.value);
}
}
if (nestedErrors.length) {
return { err: { code: "type.tuple.invalid_elements", value: data, message: "Tuple validation failed.", nestedErrors: nestedErrors } };
}
return { err: null, value: values };
}
};
},
instance: function (ctor) {
return {
version: 1,
definition: {
type: "instance",
settings: { className: "name" in ctor ? ctor.name : "" }
},
eval: function (value) {
if (!(value instanceof ctor)) {
return { err: { code: "type.instance.not_an_instance_of", value: value, message: "Not an instance of \"" + ctor.name + "\"." } };
}
return { err: null, value: value };
}
};
},
numeric: {
version: 1,
definition: {
type: "numeric"
},
eval: function (value) {
var num = Number(value) + 0;
if (!Number.isFinite(num)) {
return { err: { code: "type.numeric.not_a_finite_number", value: value, message: "Not a finite number." } };
}
return { err: null, value: num };
}
},
booleanKey: function (keys, options) {
var caseInsensitive = options && options.caseInsensitive;
var truthy = keys.truthy.reduce(function (r, k) {
r[caseInsensitive ? k.toLowerCase() : k] = true;
return r;
}, {});
var falsy = (keys.falsy || []).reduce(function (r, k) {
r[caseInsensitive ? k.toLowerCase() : k] = true;
return r;
}, {});
return {
version: 1,
definition: {
type: "booleanKey",
settings: {
keys: keys,
caseInsensitive: caseInsensitive
}
},
eval: function (value) {
var valueStr = caseInsensitive ? ("" + value).toLowerCase() : "" + value;
if (truthy.hasOwnProperty(valueStr)) {
return { err: null, value: true };
}
else if (!keys.falsy || falsy.hasOwnProperty(valueStr)) {
return { err: null, value: false };
}
else {
return { err: {
code: "type.booleanKey.invalid_key",
value: value,
allowed: keys.truthy.concat(keys.falsy),
message: "Not a valid boolean value."
} };
}
}
};
}
};