import { z } from "zod"; /** * Schema for a `{ $bind: "fieldName" }` reference inside a panel's component props. * Adapters and consumers can include this directly in their own union schemas; the * `bindable()` helper below is sugar for the common case. */ export const bindRefSchema = z.object({ $bind: z.string() }); export type BindRef = z.infer; /** * Sugar for `z.union([inner, bindRefSchema])`. Marks a slot as accepting either * a literal value or a `{ $bind: "fieldName" }` reference resolved against the * panel's query result. * * The schema itself is the source of truth — `validatePlan` and renderer adapters * detect bindability structurally by walking the union for a bind-ref branch. No * registry, no metadata to keep in sync. */ export function bindable(inner: T) { return z.union([inner, bindRefSchema]); } export function isBindRef(value: unknown): value is BindRef { return ( typeof value === "object" && value !== null && "$bind" in value && typeof (value as { $bind: unknown }).$bind === "string" ); } /** True if `schema` is a union containing a bind-ref-shaped branch. */ export function isBindable(schema: z.ZodType): boolean { return findBindBranch(schema) !== undefined; } /** * Returns the literal-value schema(s) of a bindable union — the type a slot * accepts when the value is *not* a bind ref. Returns `undefined` if `schema` * isn't bindable. If multiple non-bind branches exist, they are re-wrapped into * a fresh union. */ export function getBindableInner(schema: z.ZodType): z.ZodType | undefined { const branch = findBindBranch(schema); if (!branch) return undefined; if (branch.literals.length === 1) return branch.literals[0]; const [first, second, ...rest] = branch.literals; if (!first || !second) return undefined; return z.union([first, second, ...rest]); } interface BindBranch { literals: z.ZodType[]; } function findBindBranch(schema: z.ZodType): BindBranch | undefined { if (!(schema instanceof z.ZodUnion)) return undefined; const opts = schema.options as ReadonlyArray; const literals: z.ZodType[] = []; let hasBind = false; for (const opt of opts) { if (looksLikeBindRefSchema(opt)) { hasBind = true; } else { literals.push(opt); } } if (!hasBind || literals.length === 0) return undefined; return { literals }; } function looksLikeBindRefSchema(schema: z.ZodType): boolean { if (!(schema instanceof z.ZodObject)) return false; const shape = schema.shape as Record; const keys = Object.keys(shape); return keys.length === 1 && keys[0] === "$bind" && shape.$bind instanceof z.ZodString; }