import type { Json } from "atom.io/foundations/json" import type { Refinery, Supported } from "./refinery.ts" import { discoverType, jsonTreeRefinery, primitiveRefinery } from "./refinery.ts" import { sprawl } from "./sprawl.ts" export function diffNumber(a: number, b: number): Delta { const sign = a < b ? `+` : `-` return { summary: `${sign}${Math.abs(a - b)} (${a} → ${b})`, } } export function diffString(a: string, b: string): Delta { const sign = a.length < b.length ? `+` : `-` return { summary: `${sign}${Math.abs(a.length - b.length)} ("${a}" → "${b}")`, } } export function diffBoolean(a: boolean, b: boolean): Delta { return { summary: `${a} → ${b}`, } } export function diffObject( a: Json.Tree.Object, b: Json.Tree.Object, recurse: Diff, ): Delta { let summary = `` const added: Delta[`added`] = [] const removed: Delta[`removed`] = [] const changed: Delta[`changed`] = [] sprawl(a, (path, nodeA) => { let key: string for (key of path) { const nodeB = b[key] if (nodeB === undefined) { removed.push([key, JSON.stringify(nodeA)]) } else { const delta = recurse(nodeA, nodeB) if (delta.summary !== `No Change`) { changed.push([key, delta]) } } } }) sprawl(b, (path, nodeB) => { let key: string for (key of path) { const nodeA = a[key] if (nodeA === undefined) { added.push([key, JSON.stringify(nodeB)]) } } }) summary = `~${changed.length} +${added.length} -${removed.length}` return { summary, added, removed, changed, } } export function diffArray( a: Json.Tree.Array, b: Json.Tree.Array, recurse: Diff, ): Delta { return diffObject(a as any, b as any, recurse) } type Delta = { summary: string added?: [path: string, addedStringifiedValue: string][] removed?: [path: string, removedStringifiedValue: string][] changed?: [path: string, delta: Delta][] } type Diff = (a: T, b: T) => Delta type DiffTree = (a: T, b: T, recurse: Differ[`diff`]) => Delta export class Differ< Leaf extends Record, Tree extends Record, > { public leafRefinery: Refinery public treeRefinery: Refinery public leafDiffers: { [KL in keyof Leaf]: Diff> } public treeDiffers: { [KT in keyof Tree]: DiffTree> } public constructor( leafRefinery: Refinery, treeRefinery: Refinery, diffFunctions: { [KT in keyof Tree]: DiffTree> } & { [KL in keyof Leaf]: Diff> }, ) { this.leafRefinery = leafRefinery this.treeRefinery = treeRefinery this.leafDiffers = {} as any this.treeDiffers = {} as any for (const key of Object.keys(leafRefinery.supported)) { const diffFunction = diffFunctions[key] this.leafDiffers[key as keyof Leaf] = diffFunction } for (const key of Object.keys(treeRefinery.supported)) { const diffFunction = diffFunctions[key] this.treeDiffers[key as keyof Tree] = diffFunction } } public diff(a: unknown, b: unknown): Delta { if (a === b) { return { summary: `No Change` } } const aRefined = this.leafRefinery.refine(a) ?? this.treeRefinery.refine(a) const bRefined = this.leafRefinery.refine(b) ?? this.treeRefinery.refine(b) if (aRefined !== null && bRefined !== null) { if (aRefined.type === bRefined.type) { if (aRefined.type in this.leafDiffers) { const delta = this.leafDiffers[aRefined.type]( aRefined.data, bRefined.data, ) return delta } if (aRefined.type in this.treeDiffers) { const delta = this.treeDiffers[aRefined.type]( aRefined.data, bRefined.data, (x, y) => this.diff(x, y), ) return delta } } } const typeA = discoverType(a) const typeB = discoverType(b) if (typeA === typeB) { return { summary: `${typeA} → ${typeB}`, } } return { summary: `Type change: ${typeA} → ${typeB}`, } } } export const prettyJson: Differ< { number: (input: unknown) => input is number string: (input: unknown) => input is string boolean: (input: unknown) => input is boolean null: (input: unknown) => input is null }, { object: (input: unknown) => input is Json.Tree.Object array: (input: unknown) => input is Json.Tree.Array } > = new Differ(primitiveRefinery, jsonTreeRefinery, { number: diffNumber, string: diffString, boolean: diffBoolean, null: () => ({ summary: `No Change` }), object: diffObject, array: diffArray, })