UNPKG

pragmatic-fp-ts

Version:

Opinionated functional programming library with easy use in mind

187 lines (164 loc) 6.68 kB
import { all } from "./all.ts"; import { Either, isLeft, isRight, left, right } from "./Either.ts"; import { getValueOr } from "./getValueOr.ts"; import { isDataObject } from "./isDataObject.ts"; import { isFunction } from "./isFunction.ts"; import { isNil } from "./isNil.ts"; import { isNumber } from "./isNumber.ts"; import { isString } from "./isString.ts"; import { map } from "./map.ts"; import { values } from "./values.ts"; const nameProp = "__name"; type NameTag = { [key in typeof nameProp]: string }; export type ValidatorFn<T = any> = ((_: unknown) => _ is T) & NameTag; export type Validator<T> = ((x: unknown) => Either<T>) & NameTag; export type ValidatorType<V extends Validator<any>> = V extends Validator<infer T> ? T : never; const typeName = (x: unknown): string => Array.isArray(x) ? "array" : x === null ? "null object" : typeof x; const typeError = (validator: NameTag | string, x: unknown) => { const expectedType = isString(validator) ? validator : getName(validator); const receivedType = typeName(x); return new Error(`expected ${expectedType}, got ${receivedType} ${JSON.stringify(x)}`); }; type ValidatorOptions = { decode?: boolean | ((_: string) => any); }; const parseData = (x: unknown, decode: ValidatorOptions["decode"]) => { const parse = isFunction(decode) ? (decode as (_: string) => any) : decode === true ? (JSON.parse as (_: string) => any) : undefined; return isString(x) && parse ? parse(x) : x; }; const Validator = <T>( validate: ValidatorFn<T>, { decode }: ValidatorOptions = { decode: false } ): Validator<T> => assignName(getName(validate), (x: unknown) => { const xx = decode ? parseData(x, decode) : x; return validate(xx) ? right(xx as T) : left<T, Error>(typeError(validate, x)); }); const assignName = <T>(value: string, x: T): T & NameTag => { Object.defineProperty(x, nameProp, { value }); return x as T & NameTag; }; const getName = <T extends NameTag>(x: T): string => x[nameProp]; const joinArrayReasons = (coll: Either<any, Error>[]) => { const badValues = coll .reduce((msgs, m, idx) => { if (m.isLeft()) msgs.push(`${idx}: ${m.getReason()?.message}`); return msgs; }, [] as string[]) .join(", "); return `[${badValues}]`; }; const liftArray = <T>(coll: Array<Either<T, Error>>): Either<T[], Error> => all(isRight, coll) ? right(coll.map(liftValue)) : left(new Error(joinArrayReasons(coll))); const liftValue = (v: any) => Array.isArray(v) ? liftArray(v) : isDataObject(v) ? liftRecord(v) : getValueOr(undefined, v); const joinRecordReasons = (coll: [string, Either<any, Error>][]) => { const badFields = coll .reduce((msgs, [key, value]) => { if (isLeft(value)) msgs.push(`${key}: ${value.getReason()?.message}`); return msgs; }, [] as string[]) .join(", "); return `{${badFields}}`; }; const liftRecord = <T extends Record<string, any>>(data: { [key in keyof T]: Either<T[key]>; }): Either<T, Error> => all(isRight, values(data as any)) ? right<any, Error>(map(liftValue, data as unknown as Record<string, Either<any, Error>>)) : left<any, Error>( new Error(joinRecordReasons(Object.entries(data as unknown as Either<any, Error>[]))) ); const array = <T>(validate: Validator<T>) => { const contentType = `Array<${getName(validate as any)}>`; return assignName( contentType, (x: unknown, { decode }: ValidatorOptions = { decode: false }): Either<T[], Error> => { const xx = decode ? parseData(x, decode) : x; return Array.isArray(xx) ? liftValue(xx.map(validate)) : left(typeError(contentType, x)); } ); }; const record = <T extends Record<string, any>>(schema: { [key in keyof T]: Validator<T[key]>; }): Validator<T> => { const recordDataType = Object.entries(schema) .map(([k, v]) => `${k}: ${getName(v)}`) .join(", "); const recordType = `Record<{${recordDataType}}>`; return assignName( recordType, (x: unknown, { decode }: ValidatorOptions = { decode: false }): Either<T, Error> => { const validateRecord = (y: any) => Object.fromEntries(Object.entries(schema).map(([k, decode]) => [k, decode(y[k])])); const xx = decode ? parseData(x, decode) : x; return isDataObject(xx) ? liftValue(validateRecord(xx)) : left(typeError(recordType, x)); } ); }; const dictionary = <T>(validate: Validator<T>): Validator<Record<string, T>> => { const dictType = `Dictionary<${getName(validate)}>`; return assignName( dictType, (x: unknown, { decode }: ValidatorOptions = { decode: false }): Either<Record<string, T>> => { const decodeDict = (o: any) => Object.fromEntries(Object.entries(o).map(([k, v]) => [k, validate(v)])); const xx = decode ? parseData(x, decode) : x; return isDataObject(xx) ? liftValue(decodeDict(xx)) : left(typeError(dictType, x)); } ); }; export const from = <T>(name: string, validate: (_: unknown) => _ is T) => Validator(assignName(name, validate)); const any: Validator<any> = from("any", (_: unknown): _ is any => true); const unknown: Validator<unknown> = from("unknown", (_: unknown): _ is unknown => true); const string: Validator<string> = from("string", isString); const number: Validator<number> = from("number", isNumber); const null_: Validator<null> = from("null", (x: unknown): x is null => x === null); const undefined_: Validator<undefined> = from( "undefined", (x: unknown): x is undefined => x === undefined ); const bool: Validator<boolean> = from( "boolean", (x: unknown): x is boolean => x === true || x === false ); const nil: Validator<null | undefined> = from("nil", isNil); const dateString: Validator<string> = from( "ISO date string", (x: unknown): x is string => isString(x) && String(new Date(x)) !== "Invalid Date" ); const enum_ = <T>(allowed: Array<T>, name = `one of [${allowed.join(",")}]`): Validator<T> => { const lookup = new Set<any>(allowed); return from(name ?? `one of [${allowed.join(",")}]`, (x: unknown): x is T => lookup.has(x)); }; const oneOf = <T>(...args: Array<Validator<T>>): Validator<T> => { const typeNames = args.map(getName).join(" | "); return from(`[${typeNames}]`, (x: unknown): x is T => args.some((test) => test(x).isRight())); }; const optional = <T>(type_: Validator<T>) => oneOf<T | undefined>(type_, undefined_); const nullable = <T>(type_: Validator<T>) => oneOf<T | null>(type_, null_); export default { from, any, array, boolean: bool, dateString: dateString, dictionary, enum: enum_, nil, number, null: null_, nullable, oneOf, optional, record, string, undefined: undefined_, unknown, };