@unito/integration-sdk
Version:
Integration SDK
51 lines (43 loc) • 1.93 kB
text/typescript
import { strict as assert } from 'node:assert';
import type { RichTextNode, RichTextRoot } from '@unito/integration-api';
import type { CanRender } from '../richTextCodec.js';
// A richText codec for one field: parse provider wire to canonical AST,
// serialize canonical AST back to wire, and the renderability predicate
// the serializer uses to decide what gets sentinel-encoded.
type Field = {
canRender: CanRender;
parse: (wire: string) => RichTextRoot;
serialize: (root: RichTextRoot) => string;
};
export type ConformanceField = { key: string; module: Field };
// `{type:'break'}` and `{type:'break',children:[]}` are the same canonical
// node; integrations differ on whether they carry the empty array. Compare
// modulo that variant so conformance flags data loss, not representation.
function canonicalize(node: RichTextNode): RichTextNode {
const out: RichTextNode = { ...node };
if (out.children) {
if (out.children.length === 0) {
delete out.children;
} else {
out.children = out.children.map(canonicalize);
}
}
return out;
}
function normalize(root: RichTextRoot): RichTextRoot {
return { type: 'root', children: (root.children ?? []).map(canonicalize) };
}
// Asserts one canonical fixture round-trips through a field's codec —
// serialize→parse must not throw and must reproduce the original AST. When
// `divergence` names a documented loss, the fixture is instead asserted to
// STILL diverge, so a later codec fix fails this assertion until the now-stale
// divergence is removed.
export function assertConforms(field: ConformanceField, ast: RichTextRoot, divergence?: string): void {
const got = normalize(field.module.parse(field.module.serialize(ast)));
const want = normalize(ast);
if (divergence) {
assert.notDeepEqual(got, want, `no longer diverges — remove divergence (${divergence})`);
} else {
assert.deepEqual(got, want);
}
}