summaryrefslogtreecommitdiffhomepage
path: root/src/features/surface-host/logic/table.ts
blob: 027553cdc591454f2634ee060f08f8edc0687f89 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
 * Pure parser for the `rendererId: "table"` custom-field payload.
 *
 * This is the FRONTEND-side renderer contract for tabular custom fields: a
 * backend that wants a table emits a `custom` field with `rendererId: "table"`
 * and a payload of `{ columns: string[]; rows: (string|number)[][] }`. Cells are
 * coerced to strings. Anything that does not match the shape returns `null`, so
 * the renderer gracefully skips it (never throws on hostile/partial data).
 */

export interface TableData {
	readonly columns: readonly string[];
	readonly rows: readonly (readonly string[])[];
}

function isStringArray(v: unknown): v is unknown[] {
	return Array.isArray(v);
}

function coerceCell(v: unknown): string | null {
	if (typeof v === "string") return v;
	if (typeof v === "number" && Number.isFinite(v)) return String(v);
	if (typeof v === "boolean") return String(v);
	return null;
}

export function parseTablePayload(payload: unknown): TableData | null {
	if (typeof payload !== "object" || payload === null) return null;
	const obj = payload as Record<string, unknown>;

	const rawColumns = obj.columns;
	const rawRows = obj.rows;
	if (!isStringArray(rawColumns) || !isStringArray(rawRows)) return null;

	const columns: string[] = [];
	for (const col of rawColumns) {
		if (typeof col !== "string") return null;
		columns.push(col);
	}

	const rows: string[][] = [];
	for (const row of rawRows) {
		if (!Array.isArray(row)) return null;
		const cells: string[] = [];
		for (const cell of row) {
			const c = coerceCell(cell);
			if (c === null) return null;
			cells.push(c);
		}
		rows.push(cells);
	}

	return { columns, rows };
}