// tracing: off // based on the work of Giulio Canti in io-ts (3.x poc stage) import { Case } from "@effect-ts/system/Case" export interface Actual { readonly actual: A } export interface SingleE { readonly error: E } export interface CompoundE { readonly errors: Chunk } export type SchemaError = | LeafE | RefinementE> | RequiredKeyE> | OptionalKeyE> | StructE> | PrevE> | NextE> | CompositionE> | MissingKeysE | OptionalIndexE> | CollectionE> | NamedE> | IntersectionE> | UnionE> | MemberE> export interface LeafErrors { ParseStringE: ParseStringE ParseNumberE: ParseNumberE ParseObjectE: ParseObjectE ParseDateE: ParseDateE ParseDateMsE: ParseDateMsE ParseBoolE: ParseBoolE ParseUuidE: ParseUuidE InvalidIntegerE: InvalidIntegerE PositiveE: PositiveE UnknownRecordE: UnknownRecordE LiteralE: LiteralE NonEmptyE: NonEmptyE UnknownArrayE: UnknownArrayE TaggedUnionExtractKeyE: ExtractKeyE } export type LeafError = Extract export const defaultLeafSymbol = Symbol.for("../custom/error/defaultLeaf.js") export const toTreeSymbol = Symbol.for("../custom/error/defaultLeaf/toTree.js") export interface HasDefaultLeafE { readonly [toTreeSymbol]: Tree } // @ts-expect-error export abstract class DefaultLeafE extends Case implements HasDefaultLeafE { readonly [defaultLeafSymbol] = defaultLeafSymbol abstract get [toTreeSymbol](): Tree } export function isDefaultLeaf(t: T): t is DefaultLeafE & T { return typeof t === "object" && t != null && defaultLeafSymbol in t } export type AnyError = SchemaError // // Schema Errors // export class UnionE extends Case<{ readonly errors: Chunk }> implements CompoundE { readonly _tag = "Union" } export function unionE(errors: Chunk): UnionE { return new UnionE({ errors }) } export class ExtractKeyE extends DefaultLeafE<{ readonly field: string readonly keys: readonly string[] readonly actual: unknown }> implements Actual { readonly _tag = "ExtractKey" get [toTreeSymbol](): Tree { return tree( `cannot extract key ${this.field} from ${ JSON.stringify( this.actual ) }, expected one of ${this.keys.join(", ")}` ) } } export function extractKeyE(field: string, keys: readonly string[], actual: unknown) { return new ExtractKeyE({ actual, field, keys }) } export class LeafE extends Case<{ readonly error: E }> implements SingleE { readonly _tag = "Leaf" } export function leafE(e: E): LeafE { return new LeafE({ error: e }) } export class PrevE extends Case<{ readonly error: E }> implements SingleE { readonly _tag = "Prev" } export function prevE(e: E): PrevE { return new PrevE({ error: e }) } export class NextE extends Case<{ readonly error: E }> implements SingleE { readonly _tag = "Next" } export function nextE(e: E): NextE { return new NextE({ error: e }) } export class RefinementE extends Case<{ readonly error: E }> implements SingleE { readonly _tag = "Refinement" } export function refinementE(e: E): RefinementE { return new RefinementE({ error: e }) } export class NamedE extends Case<{ readonly name: Name readonly error: E }> implements SingleE { readonly _tag = "Named" } export function namedE(name: N, error: E): NamedE { return new NamedE({ error, name }) } export class StructE extends Case<{ readonly errors: Chunk }> implements CompoundE { readonly _tag = "Struct" } export function structE(errors: Chunk): StructE { return new StructE({ errors }) } export class CollectionE extends Case<{ readonly errors: Chunk }> implements CompoundE { readonly _tag = "Collection" } export function chunkE(errors: Chunk): CollectionE { return new CollectionE({ errors }) } export class UnknownArrayE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotArray" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected an array`) } } export function unknownArrayE(actual: unknown): UnknownArrayE { return new UnknownArrayE({ actual }) } export class RequiredKeyE extends Case<{ readonly error: E readonly key: K }> implements SingleE { readonly _tag = "RequiredKey" } export function requiredKeyE(key: K, error: E): RequiredKeyE { return new RequiredKeyE({ error, key }) } export class OptionalKeyE extends Case<{ readonly error: E readonly key: K }> implements SingleE { readonly _tag = "OptionalKey" } export function optionalKeyE(key: K, error: E): OptionalKeyE { return new OptionalKeyE({ error, key }) } export class OptionalIndexE extends Case<{ readonly index: I readonly error: E }> implements SingleE { readonly _tag = "OptionalIndex" } export function optionalIndexE(index: K, error: E): OptionalIndexE { return new OptionalIndexE({ error, index }) } export class MissingKeysE extends Case<{ readonly keys: Chunk }> { readonly _tag = "Missing" } export function missingKeysE(keys: Chunk): MissingKeysE { return new MissingKeysE({ keys }) } export class CompositionE extends Case<{ readonly errors: Chunk }> implements CompoundE { readonly _tag = "Composition" } export function compositionE(errors: Chunk): CompositionE { return new CompositionE({ errors }) } export class UnknownRecordE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotRecord" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected a record`) } } export function unknownRecordE(actual: unknown): UnknownRecordE { return new UnknownRecordE({ actual }) } export class MemberE extends Case<{ readonly member: M readonly error: E }> implements SingleE { readonly _tag = "Member" } export function memberE(member: M, error: E): MemberE { return new MemberE({ error, member }) } export class IntersectionE extends Case<{ readonly errors: Chunk }> implements CompoundE { readonly _tag = "Intersection" } export function intersectionE(errors: Chunk): IntersectionE { return new IntersectionE({ errors }) } // // Builtin // export class ParseDateE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotDateString" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected a date string`) } } export function parseDateE(actual: unknown): ParseDateE { return new ParseDateE({ actual }) } export class ParseDateMsE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotDateMs" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected a date in ms`) } } export function parseDateMsE(actual: unknown): ParseDateMsE { return new ParseDateMsE({ actual }) } export class LiteralE extends DefaultLeafE<{ readonly actual: unknown readonly literals: KS }> implements Actual { readonly _tag = "Literal" get [toTreeSymbol]() { return tree( `cannot process ${JSON.stringify(this.actual)}, expected one of ` + this.literals.join(", ") ) } } export function literalE( literals: KS, actual: unknown ): LiteralE { return new LiteralE({ literals, actual }) } export class InvalidIntegerE extends DefaultLeafE<{ readonly actual: number }> implements Actual { readonly _tag = "NotInteger" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected an integer`) } } export function invalidIntegerE(actual: number): InvalidIntegerE { return new InvalidIntegerE({ actual }) } export class PositiveE extends DefaultLeafE<{ readonly actual: number }> implements Actual { readonly _tag = "NotPositive" get [toTreeSymbol]() { return tree( `cannot process ${JSON.stringify(this.actual)}, expected to be positive` ) } } export function positiveE(actual: number): PositiveE { return new PositiveE({ actual }) } export class NonEmptyE extends DefaultLeafE<{ readonly actual: A }> implements Actual { readonly _tag = "NonEmpty" get [toTreeSymbol]() { return tree( `cannot process ${JSON.stringify(this.actual)}, expected to be not empty` ) } } export class CustomE extends DefaultLeafE<{ readonly actual: A readonly expected: string }> implements Actual { readonly _tag = "Custom" get [toTreeSymbol]() { return tree( `cannot process ${JSON.stringify(this.actual)}, expected ${this.expected}` ) } } export function nonEmptyE(actual: A): NonEmptyE { return new NonEmptyE({ actual }) } export function customE(actual: A, expected: string): CustomE { return new CustomE({ actual, expected }) } export class ParseNumberE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotNumber" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected a number`) } } export function parseNumberE(actual: unknown): ParseNumberE { return new ParseNumberE({ actual }) } export class ParseObjectE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotObject" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected an object`) } } export function parseObjectE(actual: unknown): ParseObjectE { return new ParseObjectE({ actual }) } export class ParseStringE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotString" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected an string`) } } export function parseStringE(actual: unknown): ParseStringE { return new ParseStringE({ actual }) } export class ParseBoolE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotBool" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected an string`) } } export function parseBoolE(actual: unknown): ParseBoolE { return new ParseBoolE({ actual }) } export class ParseUuidE extends DefaultLeafE<{ readonly actual: unknown }> implements Actual { readonly _tag = "NotUUID" get [toTreeSymbol]() { return tree(`cannot process ${JSON.stringify(this.actual)}, expected an UUID`) } } // // Draw // export type Forest = Chunk> export interface Tree { readonly value: A readonly forest: Forest } const empty = Chunk.empty() export function tree(value: A, forest: Forest = empty): Tree { return { value, forest } } export function toTreeWith( toTree: (e: E) => Tree ): (de: SchemaError) => Tree { const go = (de: SchemaError): Tree => { switch (de._tag) { case "Leaf": { return toTree(de.error) } case "Refinement": { return tree( `1 error(s) found while processing a refinement`, Chunk(go(de.error)) ) } case "RequiredKey": { return tree( `1 error(s) found while processing required key ${JSON.stringify(de.key)}`, Chunk(go(de.error)) ) } case "OptionalKey": { return tree( `1 error(s) found while processing optional key ${JSON.stringify(de.key)}`, Chunk(go(de.error)) ) } case "OptionalIndex": return tree( `1 error(s) found while processing optional index ${de.index}`, Chunk(go(de.error)) ) case "Collection": return tree( `${de.errors.length} error(s) found while processing a collection`, de.errors.map(go) ) case "Struct": { return tree( `${de.errors.length} error(s) found while processing a struct`, de.errors.map(go) ) } case "Union": { return tree( `${de.errors.length} error(s) found while processing a union`, de.errors.map(go) ) } case "Named": { return tree( `1 error(s) found while processing ${de.name}`, Chunk(go(de.error)) ) } case "Missing": { return tree( `${de.keys.length} error(s) found while checking keys`, de.keys.map(key => tree(`missing required key ${JSON.stringify(key)}`)) ) } case "Member": return tree( `1 error(s) found while processing member ${JSON.stringify(de.member)}`, Chunk(go(de.error)) ) case "Intersection": return tree( `${de.errors.length} error(s) found while processing an intersection`, de.errors.map(go) ) case "Prev": return go(de.error) case "Next": return go(de.error) case "Composition": { return de.errors.length === 1 ? go(de.errors.unsafeGet(0)) // less noise in the output if there's only one error : tree( `${de.errors.length} error(s) found while processing a composition`, de.errors.map(go) ) } } } return go } export function drawTree(tree: Tree): string { return tree.value + drawForest("\n", tree.forest) } function drawForest(indentation: string, forest: Chunk>): string { let r = "" const len = forest.length let tree: Tree for (let i = 0; i < len; i++) { tree = forest.unsafeGet(i) const isLast = i === len - 1 r += indentation + (isLast ? "└" : "├") + "─ " + tree.value r += drawForest(indentation + (len > 1 && !isLast ? "│ " : " "), tree.forest) } return r } export const errorToTree = toTreeWith((e: HasDefaultLeafE) => e[toTreeSymbol]) export const drawError = flow(errorToTree, drawTree)