You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
83 lines
2.4 KiB
83 lines
2.4 KiB
/**
|
|
* Data contract system. Contracts are derived from a component's morfo
|
|
* via `registerContract(morfo)`; `assertContract` validates emitted
|
|
* `data-*` values against the contract in development (no-op in
|
|
* production).
|
|
*
|
|
* Lives in morfo because the contract IS a derivation of the morfo
|
|
* declaration. Any consumer (soma providers, eidos validators, future
|
|
* tooling) imports it from `$uix/morfo`.
|
|
*/
|
|
|
|
import { compileMorfo, type CompiledMorfo } from './compile';
|
|
import type { Morfo } from './types';
|
|
|
|
/** Internal shape of a registered contract. Not consumer-facing. */
|
|
interface DataAttrRecord {
|
|
attr: string;
|
|
values?: readonly string[];
|
|
}
|
|
|
|
interface RegisteredContract {
|
|
name: string;
|
|
parts: Record<string, DataAttrRecord[]>;
|
|
}
|
|
|
|
const CONTRACTS = new Map<string, RegisteredContract>();
|
|
|
|
/**
|
|
* Register a component contract from its morfo. The morfo is the single
|
|
* source of truth — this function only caches the lookup table used by
|
|
* `assertContract` at runtime.
|
|
*/
|
|
export function registerContract(morfo: Morfo): void {
|
|
registerCompiledContract(compileMorfo(morfo));
|
|
}
|
|
|
|
export function registerCompiledContract(compiled: CompiledMorfo): void {
|
|
CONTRACTS.set(compiled.kebab.toLowerCase(), {
|
|
name: compiled.kebab,
|
|
parts: Object.fromEntries(
|
|
[...compiled.contracts.dataAttrsByPart.entries()].map(([part, attrs]) => [
|
|
part,
|
|
attrs.map((attr) => ({ attr: attr.attr, values: attr.values }))
|
|
])
|
|
)
|
|
});
|
|
}
|
|
|
|
/** Get a registered contract. Name is normalized to lowercase. */
|
|
export function getContract(name: string): RegisteredContract | undefined {
|
|
return CONTRACTS.get(name.toLowerCase());
|
|
}
|
|
|
|
/**
|
|
* Assert that props match the component's data contract.
|
|
* No-op in production. In development, validates data-* attrs.
|
|
*/
|
|
export function assertContract(
|
|
component: string,
|
|
part: string,
|
|
props: Record<string, unknown>
|
|
): void {
|
|
if (import.meta.env?.PROD) return;
|
|
|
|
const contract = CONTRACTS.get(component.toLowerCase());
|
|
if (!contract) return;
|
|
|
|
const partContract = contract.parts[part];
|
|
if (!partContract) return;
|
|
|
|
for (const attr of partContract) {
|
|
if (attr.values && props[attr.attr] !== undefined) {
|
|
const value = String(props[attr.attr]);
|
|
if (!attr.values.includes(value)) {
|
|
console.warn(
|
|
`[soma] ${component}.${part}: "${attr.attr}" has value "${value}" ` +
|
|
`but contract expects one of: ${attr.values.join(', ')}`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|