tynder
Version:
TypeScript friendly Data validator for JavaScript.
763 lines • 29.2 kB
JavaScript
"use strict";
// Copyright (c) 2019 Shellyl_N and Authors
// license: ISC
// https://github.com/shellyln
Object.defineProperty(exports, "__esModule", { value: true });
exports.getType = exports.assertType = exports.isType = exports.validate = exports.validateRoot = void 0;
const types_1 = require("./types");
const errors_1 = require("./lib/errors");
const util_1 = require("./lib/util");
const protection_1 = require("./lib/protection");
const reporter_1 = require("./lib/reporter");
const resolver_1 = require("./lib/resolver");
const noop_1 = require("./stereotypes/noop");
function checkStereotypes(data, ty, ctx) {
if (ty.stereotype && ctx.stereotypes) {
if (ctx.stereotypes.has(ty.stereotype)) {
const stereotype = ctx.stereotypes.get(ty.stereotype);
const parsed = stereotype.tryParse(data);
if (parsed) {
return ({
value: parsed.value,
stereotype,
});
}
else {
return null;
}
}
else {
throw new Error(`Undefined stereotype is specified: ${ty.stereotype}`);
}
}
return false;
}
function forceCast(targetType, value) {
switch (targetType) {
case 'number':
if (typeof value === 'number') {
return value;
}
else {
const a = Number.parseFloat(String(value));
if (Number.isNaN(a)) {
return Number(value !== null && value !== void 0 ? value : 0);
}
else {
return a;
}
}
case 'integer':
if (typeof value === 'number' && Math.trunc(value) === value) {
return value;
}
else {
let a = Number.parseFloat(String(value));
if (Number.isNaN(a)) {
a = Number(value !== null && value !== void 0 ? value : 0);
}
return Math.trunc(a);
}
case 'bigint':
try {
return BigInt(value !== null && value !== void 0 ? value : 0);
}
catch (_a) {
return NaN;
}
case 'string':
return String(value);
case 'boolean':
return Boolean(value);
case 'undefined':
return void 0;
case 'null':
return null;
default:
return value;
}
}
function checkCustomConstraints(data, ty, ctx) {
if (ty.customConstraints && ctx.customConstraints) {
for (const ccName of ty.customConstraints) {
if (ctx.customConstraints.has(ccName)) {
const cc = ctx.customConstraints.get(ccName);
if (cc.kinds && !cc.kinds.includes(ty.kind)) {
return null;
}
if (!cc.check(data, ty.customConstraintsArgs && ty.customConstraintsArgs[ccName])) {
return null;
}
}
else {
throw new Error(`Undefined constraint is specified: ${ccName}`);
}
}
return true;
}
return false;
}
function validateNeverTypeAssertion(data, ty, ctx) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
function validateAnyTypeAssertion(data, ty, ctx) {
let chkSt = checkStereotypes(data, ty, ctx);
if (chkSt === null) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
else if (chkSt === false) {
chkSt = {
value: data,
stereotype: noop_1.noopStereotype,
};
}
const styp = chkSt.stereotype;
if (checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
// always matched
return ({ value: ctx.mapper
? ctx.mapper(styp.doCast ? chkSt.value : data, ty)
: styp.doCast ? chkSt.value : data });
}
function validateUnknownTypeAssertion(data, ty, ctx) {
let chkSt = checkStereotypes(data, ty, ctx);
if (chkSt === null) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
else if (chkSt === false) {
chkSt = {
value: data,
stereotype: noop_1.noopStereotype,
};
}
const styp = chkSt.stereotype;
if (checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
// always matched
return ({ value: ctx.mapper
? ctx.mapper(styp.doCast ? chkSt.value : data, ty)
: styp.doCast ? chkSt.value : data });
}
function validatePrimitiveTypeAssertion(data, ty, ctx) {
const chkTarget = ty.forceCast ? forceCast(ty.primitiveName, data) : data;
if (ty.primitiveName === 'null') {
if (chkTarget !== null) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
}
else if (ty.primitiveName === 'integer') {
if (typeof chkTarget !== 'number') {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
if (Math.trunc(chkTarget) !== chkTarget) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
}
else if (typeof chkTarget !== ty.primitiveName) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
// TODO: Function, DateStr, DateTimeStr
let chkSt = checkStereotypes(chkTarget, ty, ctx);
if (chkSt === null) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
else if (chkSt === false) {
chkSt = {
value: chkTarget,
stereotype: ty.forceCast ? noop_1.noopStereotype : noop_1.noopStereotype,
};
}
const styVal = chkSt.value;
const styp = chkSt.stereotype;
let err = false;
let valueRangeErr = false;
switch (typeof ty.minValue) {
case 'number':
case 'string': // TODO: bigint
if (styp.compare(styVal, styp.evaluateFormula(ty.minValue)) < 0) {
if (!valueRangeErr) {
reporter_1.reportError(types_1.ErrorTypes.ValueRangeUnmatched, data, ty, { ctx });
}
valueRangeErr = true;
err = true;
}
}
switch (typeof ty.maxValue) {
case 'number':
case 'string': // TODO: bigint
if (styp.compare(styVal, styp.evaluateFormula(ty.maxValue)) > 0) {
if (!valueRangeErr) {
reporter_1.reportError(types_1.ErrorTypes.ValueRangeUnmatched, data, ty, { ctx });
}
valueRangeErr = true;
err = true;
}
}
switch (typeof ty.greaterThanValue) {
case 'number':
case 'string': // TODO: bigint
if (styp.compare(styVal, styp.evaluateFormula(ty.greaterThanValue)) <= 0) {
if (!valueRangeErr) {
reporter_1.reportError(types_1.ErrorTypes.ValueRangeUnmatched, data, ty, { ctx });
}
valueRangeErr = true;
err = true;
}
}
switch (typeof ty.lessThanValue) {
case 'number':
case 'string': // TODO: bigint
if (styp.compare(styVal, styp.evaluateFormula(ty.lessThanValue)) >= 0) {
if (!valueRangeErr) {
reporter_1.reportError(types_1.ErrorTypes.ValueRangeUnmatched, data, ty, { ctx });
}
valueRangeErr = true;
err = true;
}
}
let valueLengthErr = false;
switch (typeof ty.minLength) {
case 'number':
if (typeof styVal !== 'string' || styVal.length < ty.minLength) {
if (!valueLengthErr) {
reporter_1.reportError(types_1.ErrorTypes.ValueLengthUnmatched, data, ty, { ctx });
}
valueLengthErr = true;
err = true;
}
}
switch (typeof ty.maxLength) {
case 'number':
if (typeof styVal !== 'string' || styVal.length > ty.maxLength) {
if (!valueLengthErr) {
reporter_1.reportError(types_1.ErrorTypes.ValueLengthUnmatched, data, ty, { ctx });
}
valueLengthErr = true;
err = true;
}
}
if (ty.pattern) {
if (typeof styVal !== 'string' || !ty.pattern.test(styVal)) {
reporter_1.reportError(types_1.ErrorTypes.ValuePatternUnmatched, data, ty, { ctx });
err = true;
}
}
if (checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
err = true;
}
const ret = !err
? { value: ctx.mapper
? ctx.mapper(styp.doCast ? chkSt.value : chkTarget, ty)
: styp.doCast ? chkSt.value : chkTarget }
: null;
return ret;
}
function validatePrimitiveValueTypeAssertion(data, ty, ctx) {
const chkTarget = ty.forceCast ? forceCast(typeof ty.value, data) : data;
let chkSt = checkStereotypes(chkTarget, ty, ctx);
if (chkSt === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
else if (chkSt === false) {
chkSt = {
value: chkTarget,
stereotype: ty.forceCast ? noop_1.noopStereotype : noop_1.noopStereotype,
};
}
const styp = chkSt.stereotype;
let ret = styp.compare(chkSt.value, styp.evaluateFormula(ty.value)) === 0
? { value: ctx.mapper
? ctx.mapper(styp.doCast ? chkSt.value : chkTarget, ty)
: styp.doCast ? chkSt.value : chkTarget }
: null;
if (!ret) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
}
if (ret && checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
ret = null;
}
return ret;
}
function validateRepeatedAssertion(data, ty, ctx) {
if (!Array.isArray(data)) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
if (typeof ty.min === 'number' && data.length < ty.min) {
reporter_1.reportError(types_1.ErrorTypes.RepeatQtyUnmatched, data, ty, { ctx });
return null;
}
if (typeof ty.max === 'number' && data.length > ty.max) {
reporter_1.reportError(types_1.ErrorTypes.RepeatQtyUnmatched, data, ty, { ctx });
return null;
}
const retVals = [];
for (let i = 0; i < data.length; i++) {
const x = data[i];
const r = validateRoot(x, ty.repeated, ctx, i);
if (!r) {
return null;
}
retVals.push(r.value);
}
if (checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
return { value: retVals };
}
function validateSequenceAssertion(data, ty, ctx) {
if (!Array.isArray(data)) {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
return null;
}
let dIdx = 0, // index of data
sIdx = 0; // index of types
let spreadLen = 0;
let optionalOmitted = false;
const checkSpreadQuantity = (ts, index) => {
if (typeof ts.min === 'number' && spreadLen < ts.min) {
reporter_1.reportErrorWithPush(spreadLen === 0 ?
types_1.ErrorTypes.TypeUnmatched :
types_1.ErrorTypes.RepeatQtyUnmatched, data, [ts, index], { ctx });
return null;
}
if (typeof ts.max === 'number' && spreadLen > ts.max) {
reporter_1.reportErrorWithPush(types_1.ErrorTypes.RepeatQtyUnmatched, data, [ts, index], { ctx });
return null;
}
return ts;
};
const checkOptionalQuantity = (ts, index) => {
if (spreadLen === 0) {
// All subsequent 'optional' assertions should be 'spreadLen === 0'.
optionalOmitted = true;
}
else if (optionalOmitted) {
reporter_1.reportErrorWithPush(types_1.ErrorTypes.RepeatQtyUnmatched, data, [ts, index], { ctx });
return null;
}
else if (spreadLen > 1) {
reporter_1.reportErrorWithPush(types_1.ErrorTypes.RepeatQtyUnmatched, data, [ts, index], { ctx });
return null;
}
return ts;
};
const retVals = [];
while (dIdx < data.length && sIdx < ty.sequence.length) {
const ts = ty.sequence[sIdx];
if (ts.kind === 'spread') {
const savedErrLen = ctx.errors.length;
const r = validateRoot(data[dIdx], ts.spread, ctx, dIdx);
if (r) {
retVals.push(r.value);
dIdx++;
spreadLen++;
}
else {
// End of spreading
// rollback reported errors
ctx.errors.length = savedErrLen;
if (!checkSpreadQuantity(ts, dIdx)) {
return null;
}
spreadLen = 0;
sIdx++;
}
}
else if (ts.kind === 'optional') {
const savedErrLen = ctx.errors.length;
const r = validateRoot(data[dIdx], ts.optional, ctx, dIdx);
if (r) {
retVals.push(r.value);
dIdx++;
spreadLen++;
}
else {
// End of spreading
// rollback reported errors
ctx.errors.length = savedErrLen;
if (!checkOptionalQuantity(ts, dIdx)) {
return null;
}
spreadLen = 0;
sIdx++;
}
}
else {
const r = validateRoot(data[dIdx], ts, ctx, dIdx);
if (r) {
retVals.push(r.value);
dIdx++;
sIdx++;
}
else {
return null;
}
}
}
while (sIdx < ty.sequence.length) {
const ts = ty.sequence[sIdx];
if (ts.kind === 'spread') {
if (!checkSpreadQuantity(ts, dIdx)) {
return null;
}
spreadLen = 0;
sIdx++;
}
else if (ts.kind === 'optional') {
if (!checkOptionalQuantity(ts, dIdx)) {
return null;
}
spreadLen = 0;
sIdx++;
}
else {
reporter_1.reportErrorWithPush(types_1.ErrorTypes.RepeatQtyUnmatched, data, [ts, dIdx], { ctx });
return null;
}
}
const ret = data.length === dIdx ? { value: retVals } : null;
if (!ret) {
reporter_1.reportError(types_1.ErrorTypes.SequenceUnmatched, data, ty, { ctx });
}
if (ret && checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
return ret;
}
function validateOneOfAssertion(data, ty, ctx) {
let choosed = false;
const savedCtxRecordTypeFieldValidated = ctx.recordTypeFieldValidated;
ctx.recordTypeFieldValidated = false;
const savedErrLen = ctx.errors.length;
let count = 0;
let firstErrLen = savedErrLen;
for (const tyOne of ty.oneOf) {
const r = validateRoot(data, tyOne, ctx);
if (r) {
// rollback reported errors
ctx.errors.length = savedErrLen;
ctx.recordTypeFieldValidated = savedCtxRecordTypeFieldValidated;
return r;
}
if (ctx.recordTypeFieldValidated) {
if (count !== 0) {
const e2 = ctx.errors.slice(firstErrLen);
ctx.errors.length = savedErrLen;
ctx.errors.push(...e2);
}
choosed = true;
break;
}
if (count === 0) {
firstErrLen = ctx.errors.length;
}
else {
ctx.errors.length = firstErrLen;
}
count++;
}
if (!choosed) {
if (!ctx.checkAll) {
// rollback reported errors
ctx.errors.length = savedErrLen;
}
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
}
ctx.recordTypeFieldValidated = savedCtxRecordTypeFieldValidated;
return null;
}
function validateEnumAssertion(data, ty, ctx) {
for (const v of ty.values) {
if (data === v[1]) {
return ({ value: ctx.mapper ? ctx.mapper(data, ty) : data });
}
}
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
function validateObjectAssertion(data, ty, ctx) {
let retVal = Array.isArray(data) ? [...data] : Object.assign({}, data);
const revMembers = ty.members.slice().reverse();
for (const x of ty.members) {
if (ty.members.find(m => m[0] === x[0]) !== revMembers.find(m => m[0] === x[0])) {
reporter_1.reportError(types_1.ErrorTypes.InvalidDefinition, data, ty, { ctx });
throw new errors_1.ValidationError(`Duplicated member is found: ${x[0]} in ${ty.name || '(unnamed)'}`, ty, ctx);
}
}
if (data === null || typeof data !== 'object') {
reporter_1.reportError(types_1.ErrorTypes.TypeUnmatched, data, ty, { ctx });
if (ctx && ctx.checkAll) {
retVal = null;
}
else {
return null;
}
}
else {
const dataMembers = new Set();
if (ctx.noAdditionalProps || ty.additionalProps && 0 < ty.additionalProps.length) {
if (!Array.isArray(data)) {
for (const m in data) {
if (Object.prototype.hasOwnProperty.call(data, m)) {
dataMembers.add(m);
}
}
}
}
if (ctx.noAdditionalProps && Array.isArray(data) && 0 < data.length) {
const aps = ty.additionalProps || [];
if (aps.filter(x => x[0].includes('number')).length === 0) {
reporter_1.reportError(types_1.ErrorTypes.AdditionalPropUnmatched, data, ty, {
ctx,
substitutions: [['addtionalProps', '[number]']],
});
if (ctx && ctx.checkAll) {
retVal = null;
}
else {
return null;
}
}
}
for (const x of ty.members) {
dataMembers.delete(x[0]);
if (Object.prototype.hasOwnProperty.call(data, x[0])) {
const mt = x[1].kind === 'optional' ? // TODO: set name at compile time
Object.assign(Object.assign({}, x[1].optional), { name: x[0], message: x[1].message, messages: x[1].messages, messageId: x[1].messageId }) : x[1];
const ret = validateRoot(data[x[0]], mt, ctx);
if (ret) {
if (retVal) {
if (protection_1.isUnsafeVarNames(retVal, x[0])) {
continue;
}
retVal[x[0]] = ret.value;
if (mt.isRecordTypeField) {
ctx.recordTypeFieldValidated = true;
}
}
}
else {
if (ctx && ctx.checkAll) {
retVal = null;
}
else {
return null;
}
}
}
else {
if (x[1].kind !== 'optional') {
reporter_1.reportErrorWithPush(types_1.ErrorTypes.Required, data, [x[1], void 0], { ctx });
if (ctx && ctx.checkAll) {
retVal = null;
}
else {
return null;
}
}
}
}
if (ty.additionalProps && 0 < ty.additionalProps.length) {
function* getAdditionalMembers() {
for (const m of dataMembers.values()) {
yield m;
}
if (Array.isArray(data)) {
for (let i = 0; i < data.length; i++) {
yield String(i);
}
}
}
for (const m of getAdditionalMembers()) {
let allowImplicit = false;
const matchedAssertions = [];
for (const ap of ty.additionalProps) {
for (const pt of ap[0]) {
const at = ap[1];
if (pt === 'number') {
if (util_1.NumberPattern.test(m)) {
matchedAssertions.push(at);
}
}
else if (pt === 'string') {
matchedAssertions.push(at);
}
else {
if (pt.test(m)) {
matchedAssertions.push(at);
}
}
if (at.kind === 'optional') {
allowImplicit = true;
}
}
}
if (matchedAssertions.length === 0) {
if (allowImplicit) {
continue;
}
reporter_1.reportError(types_1.ErrorTypes.AdditionalPropUnmatched, data, ty, {
ctx,
substitutions: [['addtionalProps', m]],
});
if (ctx && ctx.checkAll) {
retVal = null;
continue;
}
else {
return null;
}
}
dataMembers.delete(m);
let hasError = false;
const savedErrLen = ctx.errors.length;
for (const at of matchedAssertions) {
const ret = validateRoot(data[m], at.kind === 'optional' ? Object.assign(Object.assign({}, at.optional), { message: at.message, messages: at.messages, messageId: at.messageId, name: m }) : Object.assign(Object.assign({}, at), { name: m }), ctx);
if (ret) {
if (retVal) {
hasError = false;
ctx.errors.length = savedErrLen;
if (protection_1.isUnsafeVarNames(retVal, m)) {
continue;
}
retVal[m] = ret.value;
}
break;
}
else {
hasError = true;
}
}
if (hasError) {
if (ctx && ctx.checkAll) {
retVal = null;
}
else {
return null;
}
}
}
}
if (ctx.noAdditionalProps && 0 < dataMembers.size) {
reporter_1.reportError(types_1.ErrorTypes.AdditionalPropUnmatched, data, ty, {
ctx,
substitutions: [['addtionalProps', Array.from(dataMembers.values()).join(', ')]],
});
if (ctx && ctx.checkAll) {
retVal = null;
}
else {
return null;
}
}
}
if (!retVal) {
// TODO: Child is unmatched. reportError?
// TODO: report object's custom error message
}
if (retVal && checkCustomConstraints(data, ty, ctx) === null) {
reporter_1.reportError(types_1.ErrorTypes.ValueUnmatched, data, ty, { ctx });
return null;
}
return retVal ? { value: (ctx && ctx.mapper) ? ctx.mapper(retVal, ty) : retVal } : null;
}
function validateRoot(data, ty, ctx, dataIndex) {
try {
ctx.typeStack.push(typeof dataIndex === 'number' || typeof dataIndex === 'string' ?
[ty, dataIndex] : ty);
switch (ty.kind) {
case 'never':
return validateNeverTypeAssertion(data, ty, ctx);
case 'any':
return validateAnyTypeAssertion(data, ty, ctx);
case 'unknown':
return validateUnknownTypeAssertion(data, ty, ctx);
case 'primitive':
return validatePrimitiveTypeAssertion(data, ty, ctx);
case 'primitive-value':
return validatePrimitiveValueTypeAssertion(data, ty, ctx);
case 'repeated':
return validateRepeatedAssertion(data, ty, ctx);
case 'sequence':
return validateSequenceAssertion(data, ty, ctx);
case 'one-of':
return validateOneOfAssertion(data, ty, ctx);
case 'enum':
return validateEnumAssertion(data, ty, ctx);
case 'object':
return validateObjectAssertion(data, ty, ctx);
case 'symlink':
if (ctx.schema) {
return validateRoot(data, resolver_1.resolveSymbols(ctx.schema, ty, { nestLevel: 0, symlinkStack: [] }), ctx);
}
reporter_1.reportError(types_1.ErrorTypes.InvalidDefinition, data, ty, { ctx });
throw new errors_1.ValidationError(`Unresolved symbol '${ty.symlinkTargetName}' is appeared.`, ty, ctx);
case 'operator':
if (ctx.schema) {
return validateRoot(data, resolver_1.resolveSymbols(ctx.schema, ty, { nestLevel: 0, symlinkStack: [] }), ctx);
}
reporter_1.reportError(types_1.ErrorTypes.InvalidDefinition, data, ty, { ctx });
throw new errors_1.ValidationError(`Unresolved type operator is found: ${ty.operator}`, ty, ctx);
case 'spread':
case 'optional':
reporter_1.reportError(types_1.ErrorTypes.InvalidDefinition, data, ty, { ctx });
throw new errors_1.ValidationError(`Unexpected type assertion: ${ty.kind}`, ty, ctx);
default:
reporter_1.reportError(types_1.ErrorTypes.InvalidDefinition, data, ty, { ctx });
throw new errors_1.ValidationError(`Unknown type assertion: ${ty.kind}`, ty, ctx);
}
}
finally {
ctx.typeStack.pop();
}
}
exports.validateRoot = validateRoot;
function validate(data, ty, ctx) {
const ctx2 = Object.assign({ errors: [], typeStack: [] }, (ctx || {}));
try {
return validateRoot(data, ty, ctx2);
}
finally {
if (ctx) {
ctx.errors = ctx2.errors;
}
}
}
exports.validate = validate;
function isType(data, ty, ctx) {
return (!!validate(data, ty, ctx));
}
exports.isType = isType;
function assertType(data, ty, ctx) {
if (!validate(data, ty, ctx)) {
throw new Error(`Assertion failed: Expected data should be of type "${ty.typeName || ty.name || '?'}".`);
}
}
exports.assertType = assertType;
function getType(schema, name) {
var _a;
if (schema.has(name)) {
return (_a = schema.get(name)) === null || _a === void 0 ? void 0 : _a.ty;
}
throw new Error(`Undefined type name is referred: ${name}`);
}
exports.getType = getType;
//# sourceMappingURL=validator.js.map