import { DateTime } from 'luxon'; export type Validator = (val: any) => T; export type ObjectSpec = Record> & { [K in keyof T]: Validator; }; function repr(val: any): string { return `\`${JSON.stringify(val)}\``; } export function object( validator: ObjectSpec, includes: string[] = Object.keys(validator), excludes: string[] = [] ): Validator { return (val: any): T => { if (!val || typeof val != 'object') { throw new Error(`Expected an object but received ${repr(val)}`); } let result: any = {}; for (let field of includes) { if (excludes.includes(field)) { continue; } let fieldValidator = validator[field]; if (!fieldValidator) { continue; } try { result[field] = fieldValidator(val[field]); } catch (e) { throw new Error( `Error validating field '${field}': ${e instanceof Error ? e.message : e}` ); } } return result; }; } export function array(validator: Validator): Validator { return (val: any): T[] => { if (!Array.isArray(val)) { throw new Error(`Expected an array but received ${repr(val)}`); } try { return val.map(validator); } catch (e) { throw new Error( `Error validating array: ${e instanceof Error ? e.message : e}` ); } }; } function typedValidator(type: string): Validator { return (val: any): T => { if (val === null || typeof val != type) { throw new Error(`Expected a ${type} but received ${repr(val)}`); } return val; }; } export function datetime(val: any): DateTime { if (typeof val != 'string') { throw new Error(`Expected an ISO-8601 string but received ${repr(val)}`); } let dt = DateTime.fromISO(val); if (!dt.isValid) { throw new Error(`Expected an ISO-8601 string but received ${repr(val)}`); } return dt; } export const boolean = typedValidator('boolean'); export const int = typedValidator('number'); export const double = typedValidator('number'); export const string = typedValidator('string'); export function base64(val: any): Buffer { if (typeof val != 'string') { throw new Error( `Expected a base64 encoded string but received ${repr(val)}` ); } try { return Buffer.from(val, 'base64'); } catch (_) { throw new Error( `Expected a base64 encoded string but received ${repr(val)}` ); } } export function nullable(validator: Validator): Validator; export function nullable(validator: Validator, result: T): Validator; export function nullable( validator: Validator, result?: T ): Validator { return (val: any): T | null => { if (val === null) { return result ?? null; } return validator(val); }; } export function optional(validator: Validator): Validator; export function optional(validator: Validator, result: T): Validator; export function optional( validator: Validator, result?: T ): Validator { return (val: any): T | undefined => { if (val === undefined) { return result; } return validator(val); }; } export function maybeArray(validator: Validator): Validator { return (val: any): T | T[] => { if (Array.isArray(val)) { try { return val.map(validator); } catch (e) { throw new Error( `Error validating array: ${e instanceof Error ? e.message : e}` ); } } return validator(val); }; } export function intString(val: any): number { if (typeof val != 'string') { throw new Error( `Expected an integer as a string but received ${repr(val)}` ); } let value = parseInt(val, 10); if (value.toString() != val) { throw new Error( `Expected an integer as a string but received ${repr(val)}` ); } return value; } export function map( keyValidator: Validator, valueValidator: Validator ): Validator> { return (val: any): Map => { if (!val || typeof val != 'object') { throw new Error(`Expected an object but received ${repr(val)}`); } let result = new Map(); for (let [key, value] of Object.entries(val)) { result.set(keyValidator(key), valueValidator(value)); } return result; }; } export function either( first: Validator, second: Validator ): Validator { return (val: any): F | S => { let result: F | S; try { result = first(val); } catch (_) { try { result = second(val); } catch (e) { throw new Error( `Expected an ${first.name} or ${second.name} but received ${repr( val )}` ); } } return result; }; }