UNPKG

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
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;