UNPKG

tynder

Version:

TypeScript friendly Data validator for JavaScript.

755 lines 27.9 kB
// Copyright (c) 2019 Shellyl_N and Authors // license: ISC // https://github.com/shellyln import { ErrorTypes } from './types'; import { ValidationError } from './lib/errors'; import { NumberPattern } from './lib/util'; import { isUnsafeVarNames } from './lib/protection'; import { reportError, reportErrorWithPush } from './lib/reporter'; import { resolveSymbols } from './lib/resolver'; import { noopStereotype } from './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) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } function validateAnyTypeAssertion(data, ty, ctx) { let chkSt = checkStereotypes(data, ty, ctx); if (chkSt === null) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } else if (chkSt === false) { chkSt = { value: data, stereotype: noopStereotype, }; } const styp = chkSt.stereotype; if (checkCustomConstraints(data, ty, ctx) === null) { reportError(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) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } else if (chkSt === false) { chkSt = { value: data, stereotype: noopStereotype, }; } const styp = chkSt.stereotype; if (checkCustomConstraints(data, ty, ctx) === null) { reportError(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) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } } else if (ty.primitiveName === 'integer') { if (typeof chkTarget !== 'number') { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } if (Math.trunc(chkTarget) !== chkTarget) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } } else if (typeof chkTarget !== ty.primitiveName) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } // TODO: Function, DateStr, DateTimeStr let chkSt = checkStereotypes(chkTarget, ty, ctx); if (chkSt === null) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } else if (chkSt === false) { chkSt = { value: chkTarget, stereotype: ty.forceCast ? noopStereotype : 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) { reportError(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) { reportError(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) { reportError(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) { reportError(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) { reportError(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) { reportError(ErrorTypes.ValueLengthUnmatched, data, ty, { ctx }); } valueLengthErr = true; err = true; } } if (ty.pattern) { if (typeof styVal !== 'string' || !ty.pattern.test(styVal)) { reportError(ErrorTypes.ValuePatternUnmatched, data, ty, { ctx }); err = true; } } if (checkCustomConstraints(data, ty, ctx) === null) { reportError(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) { reportError(ErrorTypes.ValueUnmatched, data, ty, { ctx }); return null; } else if (chkSt === false) { chkSt = { value: chkTarget, stereotype: ty.forceCast ? noopStereotype : 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) { reportError(ErrorTypes.ValueUnmatched, data, ty, { ctx }); } if (ret && checkCustomConstraints(data, ty, ctx) === null) { reportError(ErrorTypes.ValueUnmatched, data, ty, { ctx }); ret = null; } return ret; } function validateRepeatedAssertion(data, ty, ctx) { if (!Array.isArray(data)) { reportError(ErrorTypes.TypeUnmatched, data, ty, { ctx }); return null; } if (typeof ty.min === 'number' && data.length < ty.min) { reportError(ErrorTypes.RepeatQtyUnmatched, data, ty, { ctx }); return null; } if (typeof ty.max === 'number' && data.length > ty.max) { reportError(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) { reportError(ErrorTypes.ValueUnmatched, data, ty, { ctx }); return null; } return { value: retVals }; } function validateSequenceAssertion(data, ty, ctx) { if (!Array.isArray(data)) { reportError(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) { reportErrorWithPush(spreadLen === 0 ? ErrorTypes.TypeUnmatched : ErrorTypes.RepeatQtyUnmatched, data, [ts, index], { ctx }); return null; } if (typeof ts.max === 'number' && spreadLen > ts.max) { reportErrorWithPush(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) { reportErrorWithPush(ErrorTypes.RepeatQtyUnmatched, data, [ts, index], { ctx }); return null; } else if (spreadLen > 1) { reportErrorWithPush(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 { reportErrorWithPush(ErrorTypes.RepeatQtyUnmatched, data, [ts, dIdx], { ctx }); return null; } } const ret = data.length === dIdx ? { value: retVals } : null; if (!ret) { reportError(ErrorTypes.SequenceUnmatched, data, ty, { ctx }); } if (ret && checkCustomConstraints(data, ty, ctx) === null) { reportError(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; } reportError(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 }); } } reportError(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])) { reportError(ErrorTypes.InvalidDefinition, data, ty, { ctx }); throw new ValidationError(`Duplicated member is found: ${x[0]} in ${ty.name || '(unnamed)'}`, ty, ctx); } } if (data === null || typeof data !== 'object') { reportError(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) { reportError(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 (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') { reportErrorWithPush(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 (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; } reportError(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 (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) { reportError(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) { reportError(ErrorTypes.ValueUnmatched, data, ty, { ctx }); return null; } return retVal ? { value: (ctx && ctx.mapper) ? ctx.mapper(retVal, ty) : retVal } : null; } export 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, resolveSymbols(ctx.schema, ty, { nestLevel: 0, symlinkStack: [] }), ctx); } reportError(ErrorTypes.InvalidDefinition, data, ty, { ctx }); throw new ValidationError(`Unresolved symbol '${ty.symlinkTargetName}' is appeared.`, ty, ctx); case 'operator': if (ctx.schema) { return validateRoot(data, resolveSymbols(ctx.schema, ty, { nestLevel: 0, symlinkStack: [] }), ctx); } reportError(ErrorTypes.InvalidDefinition, data, ty, { ctx }); throw new ValidationError(`Unresolved type operator is found: ${ty.operator}`, ty, ctx); case 'spread': case 'optional': reportError(ErrorTypes.InvalidDefinition, data, ty, { ctx }); throw new ValidationError(`Unexpected type assertion: ${ty.kind}`, ty, ctx); default: reportError(ErrorTypes.InvalidDefinition, data, ty, { ctx }); throw new ValidationError(`Unknown type assertion: ${ty.kind}`, ty, ctx); } } finally { ctx.typeStack.pop(); } } export 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; } } } export function isType(data, ty, ctx) { return (!!validate(data, ty, ctx)); } export 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 || '?'}".`); } } export 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}`); } //# sourceMappingURL=validator.js.map