checkValue
Check a value against a schema; returns what's wrong, or undefined if it fits.
import { describe, summary } from "#codebase/tcmts/core/schema"; import type { Schema } from "#codebase/tcmts/core/schema"; export function checkValue(s: Schema, v: unknown, at: string, defs: Schema[]): string | undefined { const bad = (expected: string) => `${at} should be ${expected}, got ${describe(v)}`; switch (s.t) { case "any": return undefined; case "never": return `${at} can't be passed`; case "unsendable": return `${at} is ${s.what}, which can't be sent over the network`; case "string": case "number": case "boolean": case "bigint": return typeof v === s.t ? undefined : bad(`a ${s.t}`); case "null": return v === null ? undefined : bad("null"); case "undefined": return v === undefined ? undefined : bad("undefined"); case "date": return v instanceof Date && !Number.isNaN(v.getTime()) ? undefined : bad("a Date"); case "literal": return v === s.value ? undefined : bad(JSON.stringify(s.value)); case "union": return s.of.some((u) => checkValue(u, v, at, defs) === undefined) ? undefined : bad(s.of.map(summary).join(" | ")); case "ref": return checkValue(defs[s.id]!, v, at, defs); case "array": if (!Array.isArray(v)) return bad("an array"); for (let i = 0; i < v.length; i++) { const e = checkValue(s.of, v[i], `${at}[${i}]`, defs); if (e) return e; } return undefined; case "tuple": { if (!Array.isArray(v)) return bad("a tuple"); const min = s.of.length - s.optional; if (v.length < min || (!s.rest && v.length > s.of.length)) return bad(`a tuple of ${s.rest ? `at least ${min}` : s.optional ? `${min} to ${s.of.length}` : s.of.length} items`); for (let i = 0; i < v.length; i++) { const e = checkValue(i < s.of.length ? s.of[i]! : s.rest!, v[i], `${at}[${i}]`, defs); if (e) return e; } return undefined; } case "map": if (!(v instanceof Map)) return bad("a Map"); for (const [k, x] of v) { const e = checkValue(s.key, k, `${at} key`, defs) ?? checkValue(s.value, x, `${at}.get(${JSON.stringify(k)})`, defs); if (e) return e; } return undefined; case "set": if (!(v instanceof Set)) return bad("a Set"); for (const x of v) { const e = checkValue(s.of, x, `${at} item`, defs); if (e) return e; } return undefined; case "object": { if (typeof v !== "object" || v === null || Array.isArray(v) || v instanceof Date || v instanceof Map || v instanceof Set) return bad("an object"); const o = v as Record<string, unknown>; for (const [k, p] of Object.entries(s.props)) { if (!(k in o) || o[k] === undefined) { if (p.optional || checkValue(p.s, undefined, "", defs) === undefined) continue; return `${at}.${k} is missing`; } const e = checkValue(p.s, o[k], `${at}.${k}`, defs); if (e) return e; } for (const k of Object.keys(o)) { if (k in s.props) continue; // unexpected fields are refused rather than passed along: a server function only gets what it declared if (!s.index) return `${at}.${k} isn't a field it accepts`; const e = checkValue(s.index, o[k], `${at}.${k}`, defs); if (e) return e; } return undefined; } } }