jet-validators
Version:
Fast, zero-dependency TypeScript validators and utilities for runtime checks, parsing, and lightweight object schema validation.
389 lines (388 loc) • 13.4 kB
JavaScript
import { isObject } from '../../basic.js';
import { isTransformFn, } from '../simple-utils.js';
import deepClone from './deepClone.js';
import { isParseErrorArray, setIsParseErrorArray, } from './mark-parseError-array.js';
import { isSafe } from './mark-safe.js';
import testObjectCore, { isTestObjectCoreFn, } from './testObjectCore.js';
const ROOT_TYPE_INVALID = '<root-type-invalid>';
export const SAFETY = {
Loose: 1,
Normal: 2,
Strict: 3,
};
export const ERRORS = {
NotOptional: 'Root argument is undefined but not optional.',
NotNullable: 'Root argument is null but not nullable.',
NotObject: 'Root argument is not an object',
NotArray: 'Root argument is not an array.',
ValidatorFn: 'Validator function returned false.',
StrictSafety: 'Strict mode found an unknown or invalid property.',
SchemaProp(key) {
return `Schema property "${key}" must be a function or nested schema.`;
},
};
const parserCache = new WeakMap();
function parseObjectCore(isOptional, isNullable, isArray, schema, safety, onError) {
const parser = getCompiledParser(schema, safety);
return (param, localOnError) => {
const errorCb = onError ?? localOnError, errors = errorCb ? setIsParseErrorArray([]) : null;
const result = parseObjectCoreHelper(isOptional, isNullable, isArray, parser, errors, param);
if (result === false) {
if (!!errors && errors.length > 0) {
errorCb?.(errors);
}
return false;
}
else {
return result;
}
};
}
function getCompiledParser(schema, safety) {
let safetyMap = parserCache.get(schema);
if (!safetyMap) {
safetyMap = new Map();
parserCache.set(schema, safetyMap);
}
let parser = safetyMap.get(safety);
if (!parser) {
parser = setupValidatorParser(schema, safety);
safetyMap.set(safety, parser);
}
return parser;
}
function setupValidatorParser(schema, safety) {
const keys = Object.keys(schema), keySet = Object.create(null), argNames = [
'deepClone',
'appendNestedErrors',
'ERRORS',
'formatCaughtError',
'keySet',
'isParseErrorArray',
], argValues = [
deepClone,
appendNestedErrors,
ERRORS,
formatCaughtError,
keySet,
isParseErrorArray,
], fieldBlocks = [];
let fnIndex = 0;
const addFnRef = (fn) => {
const fnName = `fn${fnIndex++}`;
argNames.push(fnName);
argValues.push(fn);
return fnName;
};
for (let i = 0; i < keys.length; i++) {
const key = keys[i], schemaValue = schema[key];
keySet[key] = true;
if (typeof schemaValue === 'function') {
const name = schemaValue.name || '<anonymous>';
if (isSafe(schemaValue)) {
fieldBlocks.push(buildSafeBlock(key, name, addFnRef(schemaValue)));
}
else if (isTestObjectCoreFn(schemaValue)) {
fieldBlocks.push(buildNestedTestBlock(key, addFnRef(schemaValue)));
}
else if (isTransformFn(schemaValue)) {
fieldBlocks.push(buildTransformBlock(key, name, addFnRef(schemaValue)));
}
else {
fieldBlocks.push(buildUnsafeBlock(key, name, addFnRef(schemaValue)));
}
}
else if (typeof schemaValue === 'object') {
const nestedFn = testObjectCore(false, false, false, schemaValue, safety);
fieldBlocks.push(buildNestedTestBlock(key, addFnRef(nestedFn)));
}
else {
throw new Error(ERRORS.SchemaProp(key));
}
}
if (safety === SAFETY.Strict) {
fieldBlocks.push(buildStrictBlock());
}
else if (safety === SAFETY.Loose) {
fieldBlocks.push(buildLooseBlock());
}
const body = [
'"use strict";',
'return function parse(param, errors) {',
' const clean = {};',
' let isValid = true;',
' const hasOwn = Object.prototype.hasOwnProperty;',
...fieldBlocks,
' return isValid ? clean : false;',
'};',
].join('\n');
const factory = new Function(...argNames, body);
return factory(...argValues);
}
function parseObjectCoreHelper(isOptional, isNullable, isArray, parser, errors, param) {
if (param === undefined) {
if (isOptional) {
return undefined;
}
else {
errors?.push({
info: ERRORS.NotOptional,
functionName: '<optional>',
value: undefined,
key: ROOT_TYPE_INVALID,
});
return false;
}
}
if (param === null) {
if (isNullable) {
return null;
}
else {
errors?.push({
info: ERRORS.NotNullable,
functionName: '<nullable>',
value: null,
key: ROOT_TYPE_INVALID,
});
return false;
}
}
if (isArray) {
if (!Array.isArray(param)) {
errors?.push({
info: ERRORS.NotArray,
functionName: '<isArray>',
value: param,
key: ROOT_TYPE_INVALID,
});
return false;
}
const paramClone = new Array(param.length);
let isValid = true;
for (let i = 0; i < param.length; i++) {
const nestedErrors = errors ? [] : null;
const result = parser(param[i], nestedErrors);
if (result === false && !errors)
return false;
if (!!nestedErrors && nestedErrors?.length > 0) {
appendNestedErrors(errors, nestedErrors, i);
}
if (result !== false && isValid) {
paramClone[i] = result;
}
else {
isValid = false;
}
}
return isValid ? paramClone : false;
}
if (isObject(param)) {
return parser(param, errors);
}
else {
errors?.push({
info: ERRORS.NotObject,
functionName: '<isObject>',
value: param,
key: ROOT_TYPE_INVALID,
});
return false;
}
}
function buildSafeBlock(key, fnName, refName) {
const keyLiteral = JSON.stringify(key);
const fnLiteral = JSON.stringify(fnName);
return [
' {',
` const value = param[${keyLiteral}];`,
` const keyPresent = value !== undefined || hasOwn.call(param, ${keyLiteral});`,
` if (!${refName}(value)) {`,
' if (!errors) return false;',
' isValid = false;',
' errors.push({',
' info: ERRORS.ValidatorFn,',
` functionName: ${fnLiteral},`,
' value,',
` key: ${keyLiteral},`,
' });',
' } else if (keyPresent) {',
` clean[${keyLiteral}] =`,
" value !== null && typeof value === 'object' ? deepClone(value) : value;",
' }',
' }',
].join('\n');
}
function buildNestedTestBlock(key, refName) {
const keyLiteral = JSON.stringify(key);
return [
' {',
` let value = param[${keyLiteral}];`,
` const keyPresent = value !== undefined || hasOwn.call(param, ${keyLiteral});`,
' const bubble = errors',
` ? (nestedErrors) => appendNestedErrors(errors, nestedErrors, ${keyLiteral})`,
' : undefined;',
` const localIsValid = ${refName}(value, bubble, (nVal) => (value = nVal));`,
' if (!localIsValid) {',
' if (!errors) return false;',
' isValid = false;',
' } else if (keyPresent) {',
` clean[${keyLiteral}] = value;`,
' }',
' }',
].join('\n');
}
function buildTransformBlock(key, fnName, refName) {
const keyLiteral = JSON.stringify(key);
const fnLiteral = JSON.stringify(fnName);
return [
' {',
` let value = param[${keyLiteral}];`,
` const keyPresent = value !== undefined || hasOwn.call(param, ${keyLiteral});`,
' try {',
` const localIsValid = ${refName}(value, (tVal, innerIsValid) => {`,
' if (innerIsValid) {',
' value = tVal;',
' }',
' });',
' if (!localIsValid) throw null;',
' if (keyPresent) {',
` clean[${keyLiteral}] =`,
" value !== null && typeof value === 'object' ? deepClone(value) : value;",
' }',
' } catch (err) {',
' if (!errors) return false;',
' isValid = false;',
' const extra = formatCaughtError(err);',
' if (extra && extra.caught) {',
' errors.push({',
' info: ERRORS.ValidatorFn,',
` functionName: ${fnLiteral},`,
' value,',
' caught: extra.caught,',
` key: ${keyLiteral},`,
' });',
' } else {',
' errors.push({',
' info: ERRORS.ValidatorFn,',
` functionName: ${fnLiteral},`,
' value,',
` key: ${keyLiteral},`,
' });',
' }',
' }',
' }',
].join('\n');
}
function buildUnsafeBlock(key, fnName, refName) {
const keyLiteral = JSON.stringify(key), fnLiteral = JSON.stringify(fnName);
return [
' {',
` const value = param[${keyLiteral}];`,
` const keyPresent = value !== undefined || hasOwn.call(param, ${keyLiteral});`,
` const acceptsErrorCb = ${refName}.length > 1;`,
' let cbErrorsAppended = false;',
' const cb =',
' acceptsErrorCb && errors',
` ? (cbErrors) => {`,
' if (',
' Array.isArray(cbErrors) &&',
' isParseErrorArray(cbErrors) &&',
' cbErrors.length > 0',
' ) {',
' cbErrorsAppended = true;',
` appendNestedErrors(errors, cbErrors, ${keyLiteral});`,
' }',
' }',
' : undefined;',
' try {',
` if (!${refName}(value, cb)) throw null;`,
' if (keyPresent) {',
` clean[${keyLiteral}] =`,
" value !== null && typeof value === 'object' ? deepClone(value) : value;",
' }',
' } catch (err) {',
' if (!errors) return false;',
' isValid = false;',
' if (!cbErrorsAppended) {',
' const extra = formatCaughtError(err);',
' if (extra && extra.caught) {',
' errors.push({',
' info: ERRORS.ValidatorFn,',
` functionName: ${fnLiteral},`,
' value,',
' caught: extra.caught,',
` key: ${keyLiteral},`,
' });',
' } else {',
' errors.push({',
' info: ERRORS.ValidatorFn,',
` functionName: ${fnLiteral},`,
' value,',
` key: ${keyLiteral},`,
' });',
' }',
' }',
' }',
' }',
].join('\n');
}
function buildStrictBlock() {
return [
' {',
' const paramKeys = Object.keys(param);',
' for (let i = 0; i < paramKeys.length; i++) {',
' const key = paramKeys[i];',
' if (keySet[key]) continue;',
' if (!errors) return false;',
' isValid = false;',
' errors.push({',
' info: ERRORS.StrictSafety,',
" functionName: '<strict>',",
' value: param[key],',
' key,',
' });',
' }',
' }',
].join('\n');
}
function buildLooseBlock() {
return [
' {',
' const paramKeys = Object.keys(param);',
' for (let i = 0; i < paramKeys.length; i++) {',
' const key = paramKeys[i];',
' if (keySet[key]) continue;',
' const value = param[key];',
' clean[key] =',
" value !== null && typeof value === 'object' ? deepClone(value) : value;",
' }',
' }',
].join('\n');
}
function formatCaughtError(error) {
let errMsg = null;
if (error instanceof Error) {
errMsg = error.message;
}
else if (error !== null) {
errMsg = String(error);
}
return errMsg !== null ? { caught: errMsg } : {};
}
function appendNestedErrors(errors, nestedErrors, prepend) {
for (const error of nestedErrors) {
if (error.key === ROOT_TYPE_INVALID) {
error.key = String(prepend);
}
else if (!!error.key) {
error.keyPath = [String(prepend), error.key];
delete error.key;
}
else {
error.keyPath = [String(prepend), ...(error.keyPath ?? [])];
}
errors.push(error);
}
}
export default parseObjectCore;