export type Args = { [_: string]: any }; export interface Validator { type: string; check: (value: any) => Promise | Args | undefined; message: ( value: any, args?: Args, ) => Promise | string | undefined; } export interface ValidationError { type: string; param?: string[]; message?: string | null; args?: Args; } export type Validatable = Schema | Validator | Validator[]; export const ArraySymbol: unique symbol = Symbol("ArraySymbol"); export type Schema = { [key: string]: Validatable } | { [ArraySymbol]: Validatable; }; export async function validate( value: any, validators: Validatable, ): Promise { if (instanceofValidatorArray(validators) || instanceofValidator(validators)) { return validateValue(value, validators); } else { return validateSchema(value, validators); } } function instanceofValidator(value: any): value is Validator { return value && value.hasOwnProperty("type") && typeof value.type === "string" && value.hasOwnProperty("check") && typeof value.check === "function" && value.hasOwnProperty("message") && typeof value.message === "function"; } function instanceofValidatorArray(value: any): value is Validator[] { return Array.isArray(value) && value.every((x) => instanceofValidator(x)); } async function validateValue( value: any, validators: Validator | Validator[], ): Promise { if (!Array.isArray(validators)) { validators = [validators]; } const result: ValidationError[] = []; for (const validator of validators) { const args = await validator.check(value); if (args !== undefined) { const message = await validator.message(value, args); result.push({ type: validator.type, args, message, }); } } return result; } async function validateSchema( value: any, validators: Schema, ): Promise { if (validators.hasOwnProperty(ArraySymbol)) { const v = validators as { [ArraySymbol]: Validatable }; if (Array.isArray(value)) { const arr = await Promise.all( value.map((val) => validate(val, v[ArraySymbol])), ); const errors = arr.flatMap((val, idx) => (val ?? []).map((error) => ({ ...error, param: [`[${idx}]`, ...(error.param || [])], })) ); return errors; } else { return [{ type: "array", param: ["[]"], message: "Array expected!", args: {}, }]; } } const v = validators as { [key: string]: Validatable }; const valErrors: ValidationError[] = []; for (const prop in validators) { if (!validators.hasOwnProperty(prop)) { continue; } if (value.hasOwnProperty(prop)) { const errors = await validate(value[prop], v[prop]); valErrors.push(...(errors ?? [])); } else { valErrors.push({ type: "property", param: [prop], message: `Property '${prop}' expected but not found!`, args: { property: prop }, }); } } return valErrors; }