~ knotless Search/
↑↓ move↵ open⇧↵ all results

johnandersen777.bsky.social/atlibs

0
0
main
152 lines · 8.1 KB Raw History
// Composition parameters and exports (both hosts; Rust: rust/host/src/params.rs, same words).
//   parameters: [{ key, label?, type?, required?, description?, secret?, minimum?, maximum?, values?: { default?, enum? } }]
//     type: a module.json field type (mcp FIELD_TYPES), default "string". values holds the JSON values (an object, so
//     the record stays a lexicon `unknown`). A secret has no default and comes from `secrets`, never `inputs`.
//   instance input: { "$param": "<key>" } and { "$host": "<name>" } anywhere in it; bindParameters fills them.
//   exports: [{ nsid, instance, entry }]: the entry that serves a contract.

const TYPES = ["string", "integer", "number", "boolean", "object", "array"];
const KEY = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
const NSID = /^[a-zA-Z][a-zA-Z0-9-]*(\.[a-zA-Z0-9-]+){2,}$/;
const ENTRY = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
const ABSENT = Symbol("absent");
const plain = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
const holder = (v) => plain(v) && Object.keys(v).length === 1 && (typeof v.$param === "string" || typeof v.$host === "string");

const typed = (type, v) =>
	type === "string" ? typeof v === "string"
	: type === "integer" ? Number.isInteger(v)
	: type === "number" ? typeof v === "number" && Number.isFinite(v)
	: type === "boolean" ? typeof v === "boolean"
	: type === "array" ? Array.isArray(v)
	: plain(v);

/** Why `v` is not a value of parameter `p`, or undefined. */
function wrong(p, v) {
	const type = p.type ?? "string";
	if (!typed(type, v)) return `must be of type ${type}`;
	const choices = p.values?.enum;
	if (choices && !choices.some((c) => JSON.stringify(c) === JSON.stringify(v))) return `must be one of ${JSON.stringify(choices)}`;
	if (typeof v === "number" && p.minimum !== undefined && v < p.minimum) return `must be at least ${p.minimum}`;
	if (typeof v === "number" && p.maximum !== undefined && v > p.maximum) return `must be at most ${p.maximum}`;
	return undefined;
}

function holders(value, fn) {
	if (holder(value)) return fn(value);
	if (Array.isArray(value)) value.forEach((v) => holders(v, fn));
	else if (plain(value)) Object.values(value).forEach((v) => holders(v, fn));
}

/** validateComposition's part for parameters, placeholders and exports. */
export function validateParameters(manifest, fail) {
	if (manifest.parameters !== undefined && !Array.isArray(manifest.parameters)) fail("parameters must be a list");
	const keys = new Set();
	for (const p of manifest.parameters ?? []) {
		if (!KEY.test(p?.key ?? "") || keys.has(p.key)) fail(`parameter key ${p?.key} must be unique and match ${KEY}`);
		keys.add(p.key);
		const type = p.type ?? "string";
		if (!TYPES.includes(type)) fail(`parameter ${p.key}: type ${type} is not one of ${TYPES.join(", ")}`);
		for (const k of ["minimum", "maximum"]) if (p[k] !== undefined && !Number.isInteger(p[k])) fail(`parameter ${p.key}: ${k} must be an integer`);
		if (p.values !== undefined && !plain(p.values)) fail(`parameter ${p.key}: values must be an object`);
		const values = p.values ?? {};
		if (values.enum !== undefined && !(Array.isArray(values.enum) && values.enum.length)) fail(`parameter ${p.key}: values.enum must be a non-empty list`);
		if (p.secret === true && "default" in values) fail(`parameter ${p.key}: a secret has no default`);
		for (const v of [...(values.enum ?? []), ...("default" in values ? [values.default] : [])]) {
			const why = wrong(p, v);
			if (why) fail(`parameter ${p.key}: ${JSON.stringify(v)} ${why}`);
		}
	}
	for (const inst of manifest.instances) {
		holders(inst.input, (h) => {
			if (h.$param !== undefined && !keys.has(h.$param)) fail(`instance ${inst.name}: $param ${h.$param} is not a parameter`);
			if (h.$host !== undefined && !KEY.test(h.$host)) fail(`instance ${inst.name}: $host ${h.$host} must match ${KEY}`);
		});
	}
	if (manifest.exports !== undefined && !Array.isArray(manifest.exports)) fail("exports must be a list");
	const instances = new Set(manifest.instances.map((i) => i.name));
	const nsids = new Set();
	for (const e of manifest.exports ?? []) {
		if (!NSID.test(e?.nsid ?? "") || nsids.has(e.nsid)) fail(`export ${e?.nsid} must be a unique NSID`);
		nsids.add(e.nsid);
		if (!instances.has(e.instance) || !ENTRY.test(e.entry ?? "")) fail(`export ${e.nsid}: instance must be an instance and entry an export name`);
	}
}

/**
 * An instance's input against its module's config (module.json `config`, fields as in actions): every key one
 * of its fields, of its type (a $param's parameter of that type; a $host value is the host's); a required field
 * given. A module without config takes no input.
 */
export function checkInput(inst, mod, parameters, fail) {
	const input = inst.input ?? {};
	if (!plain(input)) fail(`instance ${inst.name}: input must be an object`);
	const fields = new Map((mod.config ?? []).map((f) => [f.key, f]));
	for (const [key, v] of Object.entries(input)) {
		const field = fields.get(key);
		if (!field) fail(`instance ${inst.name}: input ${key} is not in ${inst.module}'s config${fields.size ? ` (${[...fields.keys()].join(", ")})` : ""}`);
		const type = field.type ?? "string";
		if (holder(v)) {
			const p = v.$param !== undefined && (parameters ?? []).find((x) => x.key === v.$param);
			if (p && (p.type ?? "string") !== type) fail(`instance ${inst.name}: input ${key} is ${type}, but parameter ${p.key} is ${p.type ?? "string"}`);
		} else if (!typed(type, v)) fail(`instance ${inst.name}: input ${key} must be of type ${type}`);
	}
	for (const f of fields.values()) if (f.required === true && !Object.hasOwn(input, f.key)) fail(`instance ${inst.name}: input ${f.key} is required by ${inst.module}'s config`);
}

/**
 * The manifest with its placeholders filled: `inputs` (parameter values), `secrets` (secret parameters' values),
 * `host` (values only the host gives). Missing values take their default; a required one without either fails;
 * an optional one without is dropped (null in a list). Throws "composition: ..." on the first problem.
 */
export function bindParameters(manifest, { inputs = {}, secrets = {}, host = {} } = {}) {
	const fail = (message) => {
		throw new Error(`composition: ${message}`);
	};
	const declared = new Map((manifest.parameters ?? []).map((p) => [p.key, p]));
	for (const k of Object.keys(inputs)) {
		if (!declared.has(k)) fail(`parameter ${k} is not declared`);
		if (declared.get(k).secret === true) fail(`parameter ${k} is a secret: give it in secrets, not inputs`);
	}
	for (const k of Object.keys(secrets)) if (declared.get(k)?.secret !== true) fail(`secret ${k} is not a secret parameter`);
	const values = new Map();
	for (const p of declared.values()) {
		const given = p.secret === true ? secrets : inputs;
		const v = Object.hasOwn(given, p.key) ? given[p.key] : p.values?.default;
		if (v === undefined) {
			if (p.required === true) fail(`parameter ${p.key} is required`);
			continue;
		}
		const why = wrong(p, v);
		if (why) fail(`parameter ${p.key} ${why}`);
		values.set(p.key, v);
	}
	const swap = (value) => {
		if (holder(value)) {
			if (value.$host !== undefined) return Object.hasOwn(host, value.$host) ? host[value.$host] : fail(`host value ${value.$host} is not given by this host`);
			return values.has(value.$param) ? values.get(value.$param) : ABSENT;
		}
		if (Array.isArray(value)) return value.map((v) => ((v = swap(v)) === ABSENT ? null : v));
		if (plain(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, swap(v)]).filter(([, v]) => v !== ABSENT));
		return value;
	};
	const instances = manifest.instances.map((inst) => {
		if (inst.input === undefined) return inst;
		const input = swap(inst.input);
		const { input: _, ...rest } = inst;
		return input === ABSENT ? rest : { ...inst, input };
	});
	return { ...manifest, instances };
}

/** runCompose's guard: a placeholder still in an instance's input fails the run. */
export function checkBound(manifest) {
	for (const inst of manifest.instances) {
		holders(inst.input, (h) => {
			throw new Error(`composition: ${h.$param !== undefined ? `parameter ${h.$param}` : `host value ${h.$host}`} is not bound`);
		});
	}
}

/** The instance and entry that serve contract `nsid`, or undefined. */
export const exportOf = (manifest, nsid) => (manifest.exports ?? []).find((e) => e.nsid === nsid);