import { Union } from "./types"; import { inspect } from "util"; export class Ok { constructor(readonly value: A) {} chain(f: (_: A) => Result): Result { return f(this.value); } recover(f: (_: B) => Result): Result { return this; } } export class Err { constructor(readonly reason: B) {} chain(f: (_: A) => Result): Result { return this as any; } recover(f: (_: B) => Result): Result { return f(this.reason); } } function collect(xs: Valid[]): Valid { return xs.reduce((rs: Valid, x) => { return rs.chain((rsV) => { return x.chain((xV) => { return new Ok([...rsV, xV]); }); }); }, new Ok([])); } export type SpecType = T extends AnySpec ? U : never; export type SpecTypes = Ts extends [AnySpec] ? U : Ts extends [AnySpec, ...infer Rest] ? U | SpecTypes : never; export type RecordSpecType = R extends Record ? { [K0 in K]: SpecType } : never; type Result = Ok | Err; export class EType { constructor(readonly expected: string) {} format(): string { return `a value of type ${this.expected}`; } } export class ENoKey { constructor(readonly key: string) {} format(): string { return `the key ${JSON.stringify(this.key)} to be present`; } } export class ENotEqual { constructor(readonly expected: any) {} format(): string { return `${inspect(this.expected)}`; } } export class EAnyOf { constructor(readonly errors: Errors[]) {} format(): string { return `any of: ${this.errors.map((x) => x.format()).join(", ")}`; } } export class EPath { constructor(readonly key: string, readonly error: Errors) {} get_path_and_error() { const go = (path: string[], err: Errors): [string[], Errors] => { if (err instanceof EPath) { return go([...path, err.key], err.error); } else { return [path, err]; } }; return go([], this); } format(): string { const [path, error] = this.get_path_and_error(); return `${error.format()} at path ${path.join(".")}`; } } type Errors = EType | ENoKey | ENotEqual | EAnyOf | EPath; type Valid = Result; const failed = new (class Failed {})(); export class LazySpec { constructor(readonly thunk: () => AnySpec) {} } export type SpecFun = (_: any) => Valid; export type SpecContainer = { spec: AnySpec }; export type AnySpec = SpecFun | SpecContainer | LazySpec; export function lazy(x: () => AnySpec) { return new LazySpec(x); } function toSpec(x: AnySpec): SpecFun { if (x instanceof LazySpec) { return toSpec(x.thunk()); } else if (typeof (x as any).spec === "function") { return (x as any).spec; } else { return x as any; } } export function string(x: any): Valid { if (typeof x === "string") { return new Ok(x); } else { return new Err(new EType("string")); } } export function bigint(x: any): Valid { if (typeof x === "bigint") { return new Ok(x); } else { return new Err(new EType("bigint")); } } export function bigint_string(x: any): Valid { return string(x).chain((s) => { try { return new Ok(BigInt(s)); } catch { return new Err(new EType("bigint")); } }); } export function number(x: any): Valid { if (typeof x === "number") { return new Ok(x); } else { return new Err(new EType("number")); } } export function boolean(x: any): Valid { if (typeof x === "boolean") { return new Ok(x); } else { return new Err(new EType("boolean")); } } export function nothing(x: any): Valid { if (x == null) { return new Ok(null); } else { return new Err(new EType("nothing")); } } export function array(f0: AnySpec): SpecFun { const f = toSpec(f0); return (xs: any) => { if (Array.isArray(xs)) { return collect(xs.map(f)); } else { return new Err(new EType("array")); } }; } export function equal(x: A): SpecFun { return (value: any) => { if (value === x) { return new Ok(value); } else { return new Err(new ENotEqual(x)); } }; } export function anyOf[]>( fs: [...T] ): SpecFun> { return (value: any) => { return fs.map(toSpec).reduce((r: Result, f: SpecFun) => { return r.recover((rs: EAnyOf) => { return f(value).recover((e: Errors) => { return new Err(new EAnyOf([...rs.errors, e])); }) as Result; }); }, new Err(new EAnyOf([]))); }; } export function spec>, B>( type: A, parser: (_: RecordSpecType) => B ): SpecFun { return (value: any) => { if (value !== null && typeof value === "object") { const entries = collect( Object.entries(type).map(([k, f]: [string, any]) => { return toSpec(f)(value[k] ?? failed) .recover((e) => new Err(new EPath(k, e))) .chain((v: any) => new Ok([k, v])); }) ); return entries.chain((xs: any[]) => { const result = Object.create(null); for (const [k, v] of xs) { result[k] = v; } return new Ok(parser(result)); }); } else { return new Err(new EType("object")); } }; } export function optional(spec: AnySpec, default_value: A): SpecFun { return (value: any) => { if (value === failed) { return new Ok(default_value); } else { return toSpec(spec)(value); } }; } export function map_spec(spec: AnySpec, f: (_: A) => B): SpecFun { return (value: any) => { return toSpec(spec)(value).chain((v) => new Ok(f(v))); }; } export function parse(x: any, spec: AnySpec): A { const result = toSpec(spec)(x); if (result instanceof Ok) { return result.value; } else { throw new Error(`Failed to parse: Expected ${result.reason.format()}`); } } export function try_parse(x: any, spec: AnySpec): Valid { return toSpec(spec)(x); }