import type { ServiceMetadata } from "./types.ts"; export interface ValidationIssue { path: string; message: string; } export function parseAndValidate(raw: string, fallbackId: string): ServiceMetadata { let data: unknown; try { data = JSON.parse(raw); } catch (e) { throw new Error(`metadata.json is not valid JSON: ${(e as Error).message}`); } if (typeof data !== "object" || data === null || Array.isArray(data)) { throw new Error("metadata.json must be a JSON object"); } const meta = data as ServiceMetadata; const issues = validateMetadata(meta, fallbackId); if (issues.length > 0) { const detail = issues.map((i) => `${i.path}: ${i.message}`).join("; "); throw new Error(`invalid metadata.json (${detail})`); } meta.id = meta.id || fallbackId; return meta; } export function validateMetadata(meta: ServiceMetadata, fallbackId: string): ValidationIssue[] { const issues: ValidationIssue[] = []; const id = meta.id || fallbackId; if (!meta.name || typeof meta.name !== "string") { issues.push({ path: "name", message: "required string" }); } if (!meta.description || typeof meta.description !== "string") { issues.push({ path: "description", message: "required string" }); } if (!meta.version || typeof meta.version !== "string") { issues.push({ path: "version", message: "required string" }); } if (!meta.compose || typeof meta.compose !== "object" || Array.isArray(meta.compose)) { issues.push({ path: "compose", message: "required object (docker-compose service spec)" }); } else if (!meta.compose.image && !meta.compose.build) { issues.push({ path: "compose", message: "must define `image` (pull an image) or `build` (build from a Dockerfile)", }); } if (!/^[a-z0-9][a-z0-9-]*$/.test(id)) { issues.push({ path: "id", message: "must be kebab-case and match the folder name" }); } return issues; }