checkValue

tcmts.core.schema.checkValue · #dsdlywts · term · runs anywhere

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;
    }
  }
}

uses

used by