import type { AbstractableConstructor , arraySubclassToReadonly , BuiltinObjectKind , conform, Constructor, Domain } from "../../dev/utils/src/main.js" import { domainDescriptions , domainOf, getExactBuiltinConstructorName , objectKindDescriptions, Path , stringify } from "../../dev/utils/src/main.js" import { comparatorDescriptions } from "../nodes/primitive/range.js" import type { Bound, SizedData } from "../nodes/primitive/range.js" import type { InternalId } from "./registry.js" export class ArkTypeError extends TypeError { cause: Problems constructor(problems: Problems) { super(`${problems}`) this.cause = problems } } export abstract class Problem { data: DataWrapper path: Path abstract readonly code: ProblemCode abstract mustBe: string constructor(public rule: requirement, data: data, segments: string[]) { this.path = new Path(...segments) this.data = new DataWrapper(data) } hasCode(code: code): this is ProblemFrom { return this.code === code } get message() { return this.path.length === 0 ? capitalize(this.reason) : this.path.length === 1 && typeof this.path[0] === "number" ? `Item at index ${this.path[0]} ${this.reason}` : `${this.path} ${this.reason}` } get reason() { return `must be ${this.mustBe}${this.was ? ` (was ${this.was})` : ""}` } get was() { return `${this.data}` } toString() { return this.message } } class ProblemsArray extends Array { private readonly $arkId: InternalId = "problems" byPath: Record = {} count = 0 add(problem: Problem) { const pathKey = `${problem.path}` const existing = this.byPath[pathKey] if (existing) { if (existing.hasCode("intersection")) { existing.rule.push(problem) } else { const problemIntersection = new ProblemIntersection( [existing, problem], problem.data, problem.path ) const existingIndex = this.indexOf(existing) // If existing is found (which it always should be unless this was externally mutated), // replace it with the new problem intersection. In case it isn't for whatever reason, // just append the intersection. this[existingIndex === -1 ? this.length : existingIndex] = problemIntersection this.byPath[pathKey] = problemIntersection } } else { this.byPath[pathKey] = problem this.push(problem) } this.count++ return problem } get summary() { return this.toString() } toString() { return this.join("\n") } throw(): never { throw new ArkTypeError(this) } } export const Problems: new () => Problems = ProblemsArray export type Problems = arraySubclassToReadonly const capitalize = (s: string) => s[0].toUpperCase() + s.slice(1) export const domainsToDescriptions = (domains: Domain[]) => domains.map((objectKind) => domainDescriptions[objectKind]) export const objectKindsToDescriptions = (kinds: BuiltinObjectKind[]) => kinds.map((objectKind) => objectKindDescriptions[objectKind]) export const describeBranches = (descriptions: string[]) => { if (descriptions.length === 0) { return "never" } if (descriptions.length === 1) { return descriptions[0] } let description = "" for (let i = 0; i < descriptions.length - 1; i++) { description += descriptions[i] if (i < descriptions.length - 2) { description += ", " } } description += ` or ${descriptions[descriptions.length - 1]}` return description } export class ProblemIntersection extends Problem { readonly code = "intersection" get message() { return this.path.length ? `At ${this.path}, ${this.reason}` : this.reason } get mustBe() { return "• " + this.rule.map(({ mustBe }) => mustBe).join("\n• ") } get reason() { return `${this.data} must be...\n${this.mustBe}` } } export class DomainProblem extends Problem { readonly code = "domain" get mustBe() { return domainDescriptions[this.rule] } override get was() { return this.data.domain } } export class ProblemUnion extends Problem { readonly code = "union" get mustBe() { return describeBranches( this.rule.map( (problem) => `${problem.path} must be ${ problem.hasCode("intersection") ? describeBranches( problem.rule.map((part) => part.mustBe) ) : problem.mustBe }` ) ) } get reason() { return this.path.length ? `At ${this.path}, ${this.mustBe} (was ${this.was})` : `${this.mustBe} (was ${this.was})` } } export class CustomProblem extends Problem { readonly code = "custom" get mustBe() { return this.rule } } export const defineProblemsCode = (problems: { [code in keyof problems]: conform< problems[code], Constructor<{ readonly code: code }> > }) => problems export type KeyProblemKind = "missing" | "extraneous" export class KeyProblem extends Problem { readonly code = "key" mustBe = this.rule === "missing" ? "defined" : "extraneous" } export class RangeProblem extends Problem { readonly code = "range" get mustBe() { return `${comparatorDescriptions[this.rule.comparator]} ${ this.data.value instanceof Date ? new Date(this.rule.limit).toDateString() : this.rule.limit }${this.data.units ? ` ${this.data.units}` : ""}` } get was() { return this.data.value instanceof Date ? this.data.value.toDateString() : `${this.data.size}` } } export class RegexProblem extends Problem { readonly code = "regex" get mustBe() { return `a string matching /${this.rule}/` } } export class ClassProblem extends Problem { readonly code = "class" get mustBe() { const possibleObjectKind = getExactBuiltinConstructorName(this.rule) return possibleObjectKind ? objectKindDescriptions[possibleObjectKind] : `an instance of ${this.rule.name}` } override get was() { return this.data.className } } export class DivisorProblem extends Problem { readonly code = "divisor" get mustBe() { return this.rule === 1 ? `an integer` : `a multiple of ${this.rule}` } } export class ValueProblem extends Problem { readonly code = "value" get mustBe() { return stringify(this.rule) } } export const problemsByCode = defineProblemsCode({ domain: DomainProblem, divisor: DivisorProblem, class: ClassProblem, key: KeyProblem, range: RangeProblem, regex: RegexProblem, value: ValueProblem, custom: CustomProblem, intersection: ProblemIntersection, union: ProblemUnion }) type ProblemClasses = typeof problemsByCode export type ProblemCode = keyof ProblemClasses export type ProblemFrom = InstanceType< ProblemClasses[code] > export type ProblemRules = { [code in ProblemCode]: ProblemFrom["rule" & keyof ProblemFrom] } export type ProblemData = { [code in ProblemCode]: ProblemFrom["data" & keyof ProblemFrom] } export type ProblemParameters = ConstructorParameters< ProblemClasses[code] > export type ProblemOptions = { mustBe?: string } export type ProblemOptionsByCode = { [code in ProblemCode]?: ProblemOptions } export const sizeOf = (data: unknown) => typeof data === "string" || Array.isArray(data) ? data.length : typeof data === "number" ? data : data instanceof Date ? data.valueOf() : 0 export const unitsOf = (data: unknown) => typeof data === "string" ? "characters" : Array.isArray(data) ? "items long" : "" export class DataWrapper { constructor(public value: value) {} toString() { return stringify(this.value) } get domain() { return domainOf(this.value) } get size() { return sizeOf(this.value) } get units() { return unitsOf(this.value) } get className() { return Object(this.value).constructor.name } }