import type { Profil, ProfilVariable } from "@/engine/katalog/schema"; /** * Aus der Auswahl im Assistenten wird eine geordnete Liste: Voraussetzungen zuerst (`requires`, * transitiv), sonst die Reihenfolge der Auswahl. Dazu die Variablen mit Standardwerten und * Typprüfung. Fehler werden gesammelt, damit das Formular alle auf einmal zeigen kann. */ export type AufgeloestesProfil = { profil: Profil; werte: Record }; export type ProfilFehler = { profil: string; variable?: string; meldung: string }; export type Aufloesung = { ok: true; profile: AufgeloestesProfil[]; automatisch: string[] } | { ok: false; fehler: ProfilFehler[] }; export function profileAufloesen( auswahl: string[], verfuegbar: ReadonlyMap, familie: string, eingaben: Record> = {}, ): Aufloesung { const fehler: ProfilFehler[] = []; const reihenfolge: string[] = []; const besucht = new Set(); const inArbeit = new Set(); const besuchen = (id: string, von: string | null) => { if (besucht.has(id)) return; if (inArbeit.has(id)) { fehler.push({ profil: id, meldung: `Zyklische Voraussetzung über „${von}“.` }); return; } const profil = verfuegbar.get(id); if (!profil) { fehler.push({ profil: id, meldung: von ? `Wird von „${von}“ vorausgesetzt, ist aber nicht verfügbar.` : "Unbekanntes oder deaktiviertes Profil.", }); return; } inArbeit.add(id); for (const voraussetzung of profil.requires) besuchen(voraussetzung, id); inArbeit.delete(id); besucht.add(id); reihenfolge.push(id); }; for (const id of auswahl) besuchen(id, null); const profile: AufgeloestesProfil[] = []; for (const id of reihenfolge) { const profil = verfuegbar.get(id)!; if (!profil.families[familie]) { fehler.push({ profil: id, meldung: `Keine Umsetzung für die Familie „${familie}“.` }); continue; } const werte: Record = {}; for (const variable of profil.variables) { const ergebnis = wertPruefen(variable, eingaben[id]?.[variable.name]); if ("fehler" in ergebnis) fehler.push({ profil: id, variable: variable.name, meldung: ergebnis.fehler }); else werte[variable.name] = ergebnis.wert; } profile.push({ profil, werte }); } if (fehler.length > 0) return { ok: false, fehler }; return { ok: true, profile, automatisch: reihenfolge.filter((id) => !auswahl.includes(id)) }; } /** Werte eines Profils aus der Eingabe, mit Standardwerten und Typprüfung. */ export function variablenWerte( profil: Profil, eingabe: Record, ): { werte: Record; fehler: ProfilFehler[] } { const werte: Record = {}; const fehler: ProfilFehler[] = []; for (const variable of profil.variables) { const ergebnis = wertPruefen(variable, eingabe[variable.name]); if ("fehler" in ergebnis) fehler.push({ profil: profil.id, variable: variable.name, meldung: ergebnis.fehler }); else werte[variable.name] = ergebnis.wert; } return { werte, fehler }; } /** * Ein ins Template eingebackenes Profil muss beim Deploy nur dann nicht noch einmal laufen, wenn * seine Variablen genau die Standardwerte haben – mit denen wurde es eingebacken. */ export function mitStandardwerten(eintrag: AufgeloestesProfil): boolean { const standard = variablenWerte(eintrag.profil, {}).werte; return JSON.stringify(standard) === JSON.stringify(eintrag.werte); } /** * Verweist ein Profil auf `{{ guest.* }}` (Name, Benutzer), hängt es vom einzelnen Gast ab – im * Template stand dort etwas anderes. Solche Profile laufen beim Deploy trotzdem noch einmal; dank * Idempotenz ist das schnell (Fund: docker nahm sonst den Benutzer nicht in die Gruppe auf). */ export function gastunabhaengig(profil: Profil): boolean { return !/\{\{[^}]*\bguest\./.test(JSON.stringify(profil.families)); } function wertPruefen( variable: ProfilVariable, eingabe: unknown, ): { wert: string | number | boolean } | { fehler: string } { const roh = eingabe === undefined || eingabe === null || eingabe === "" ? variable.default : eingabe; if (roh === undefined || roh === "") { if (variable.required) return { fehler: `${variable.label} fehlt.` }; return { wert: variable.type === "boolean" ? false : "" }; } switch (variable.type) { case "boolean": if (typeof roh === "boolean") return { wert: roh }; if (roh === "true" || roh === "1") return { wert: true }; if (roh === "false" || roh === "0") return { wert: false }; return { fehler: `${variable.label}: ja oder nein erwartet.` }; case "number": { const zahl = typeof roh === "number" ? roh : Number(roh); return Number.isFinite(zahl) ? { wert: zahl } : { fehler: `${variable.label}: Zahl erwartet.` }; } case "select": return variable.options!.includes(String(roh)) ? { wert: String(roh) } : { fehler: `${variable.label}: erlaubt sind ${variable.options!.join(", ")}.` }; default: { const text = String(roh); // Ein Zeilenumbruch in einer Variable ist fast immer ein Kopierfehler – und in Konfigurationsdateien gefährlich. if (/[\r\n\0]/.test(text)) return { fehler: `${variable.label}: keine Zeilenumbrüche.` }; return { wert: text }; } } }