import { Codec } from "./codec.js" export type Metadata = Array< | { type: "atomic" name: string docs?: never factory?: never args?: never } | { type: "factory" name: string docs?: never factory: (...args: any) => Codec args: any[] } > /** Metadata for an atomic codec */ export function metadata(name: string): Metadata /** Metadata for a factory-made codec */ export function metadata( name: string, factory: (...args: A) => Codec, ...args: A ): Metadata /** Concatenate multiple metadata arrays */ export function metadata(...metadata: Metadata[]): Metadata export function metadata( ...fullArgs: | Metadata[] | [ name: string, factory?: (...args: any) => Codec, ...args: any[], ] ): Metadata { if (typeof fullArgs[0] !== "string") return fullArgs.flat() const [name, factory, ...args] = fullArgs as [name: string, factory?: (...args: any) => Codec, ...args: any[]] return [ factory ? { type: "factory", name, factory, args, } : { type: "atomic", name, }, ] } export class CodecVisitor { #fallback?: (codec: Codec) => R #visitors = new Map[number] | Function, (codec: Codec, ...args: any[]) => R>() add(codec: (...args: A) => Codec, fn: (codec: Codec, ...args: A) => R): this add(codec: Codec, fn: (codec: Codec) => R): this add(codec: Codec | Metadata[number] | Function, fn: (codec: Codec, ...args: any[]) => R): this { if (codec instanceof Codec) { codec = codec._metadata[0]! if (!codec) throw new Error("Cannot register visitor for metadata-less codec") } if (this.#visitors.has(codec)) { throw new Error("Duplicate visitor") } this.#visitors.set(codec, fn) return this } fallback(fn: (codec: Codec) => R): this { if (this.#fallback) { throw new Error("Duplicate fallback") } this.#fallback = fn return this } /** * ```ts * visitor.generic(() => * visitor.add($.array, (codec, $el) => { * ... * }) * ) * ``` */ generic(fn: (visitor: this) => void): this { fn(this) return this } visit(codec: Codec): R { for (const metadata of codec._metadata) { let visitor = this.#visitors.get(metadata) if (visitor) return visitor(codec) if (metadata.type !== "factory") continue visitor = this.#visitors.get(metadata.factory) if (visitor) return visitor(codec, ...metadata.args) } if (this.#fallback) { return this.#fallback(codec) } throw new Error("Unrecognized codec") } }