/** * VL Workflow Engine — Registry Parser & Validator * Ported from Go: workflow/registry.go * Spec: v3.15 */ /** * Parse a service signature: * "ServiceName(param1(TYPE1), param2(TYPE2)) RETURN result1(TYPE1)" */ function parseServiceSignature(sig) { // Handle nested parens: ServiceName(param1(TYPE1), param2(TYPE2)) RETURN ... const nameEnd = sig.indexOf('('); if (nameEnd < 0) throw new Error(`Invalid service signature: ${sig}`); const name = sig.slice(0, nameEnd).trim(); // Find the matching close paren for the outer params let depth = 0, paramsEnd = -1; for (let i = nameEnd; i < sig.length; i++) { if (sig[i] === '(') depth++; else if (sig[i] === ')') { depth--; if (depth === 0) { paramsEnd = i; break; } } } if (paramsEnd < 0) throw new Error(`Invalid service signature: ${sig}`); const paramsStr = sig.slice(nameEnd + 1, paramsEnd); const rest = sig.slice(paramsEnd + 1).trim(); const returnsStr = rest.startsWith('RETURN') ? rest.slice(6).trim() : ''; return { name, parameters: paramsStr ? parseNestedParamList(paramsStr) : [], returns: returnsStr ? parseNestedParamList(returnsStr) : [] }; } function parseParamList(str) { return str.split(',').map(s => s.trim()).filter(Boolean).map(s => { const m = s.match(/^(\w+)\(([^)]+)\)$/); return m ? { name: m[1], type: m[2] } : { name: s, type: 'STRING' }; }); } /** Split nested param list respecting parens: "a(TYPE), b(TYPE)" */ function parseNestedParamList(str) { const items = []; let depth = 0, start = 0; for (let i = 0; i <= str.length; i++) { if (i === str.length || (str[i] === ',' && depth === 0)) { const item = str.slice(start, i).trim(); if (item) { const m = item.match(/^(\w+)\((.+)\)$/); items.push(m ? { name: m[1], type: m[2] } : { name: item, type: 'STRING' }); } start = i + 1; } else if (str[i] === '(') depth++; else if (str[i] === ')') depth--; } return items; } /** * Parse a variable declaration: "$varName(TYPE)" or "$varName([TYPE])" */ function parseVariableDeclaration(decl) { const m = decl.match(/^(\$\w+)\((\[?\w+\]?)\)$/); if (!m) throw new Error(`Invalid variable declaration: ${decl}`); return { name: m[1], type: m[2] }; } /** * Parse a param declaration: "paramName(TYPE)" or "paramName(TYPE) = default" */ function parseParamDeclaration(decl) { const m = decl.match(/^(\w+)\((\w+)\)(?:\s*=\s*(.+))?$/); if (!m) throw new Error(`Invalid param declaration: ${decl}`); const result = { name: m[1], type: m[2], default: undefined }; if (m[3] !== undefined) { let def = m[3].trim(); // Strip surrounding quotes if ((def.startsWith('"') && def.endsWith('"')) || (def.startsWith("'") && def.endsWith("'"))) { def = def.slice(1, -1); } result.default = coerceParamDefault(result.type, def); } return result; } function coerceParamDefault(type, val) { switch (type) { case 'INT': { const n = parseInt(val, 10); return isNaN(n) ? val : n; } case 'FLOAT': { const n = parseFloat(val); return isNaN(n) ? val : n; } case 'BOOL': return val === 'true' || val === '1'; default: return val; } } /** * Validate a registry object. */ function validateRegistry(registry) { const errors = []; if (!registry) return errors; // Duplicate service names const svcNames = new Set(); for (const sig of (registry.services || [])) { try { const parsed = parseServiceSignature(sig); if (svcNames.has(parsed.name)) errors.push(`Duplicate service: ${parsed.name}`); svcNames.add(parsed.name); } catch (e) { errors.push(e.message); } } // Duplicate API IDs const apiIds = new Set(); for (const api of (registry.apis || [])) { if (!api.id) errors.push('API missing id'); else if (apiIds.has(api.id)) errors.push(`Duplicate API: ${api.id}`); else apiIds.add(api.id); if (!api.url) errors.push(`API ${api.id}: url required`); if (api.method && !['GET','POST','PUT','PATCH','DELETE'].includes(api.method.toUpperCase())) { errors.push(`API ${api.id}: invalid method ${api.method}`); } } // Duplicate params const paramNames = new Set(); for (const p of (registry.params || [])) { try { const parsed = parseParamDeclaration(p); if (paramNames.has(parsed.name)) errors.push(`Duplicate param: ${parsed.name}`); paramNames.add(parsed.name); } catch (e) { errors.push(e.message); } } // Duplicate vars const varNames = new Set(); for (const v of (registry.vars || [])) { try { const parsed = parseVariableDeclaration(v); if (varNames.has(parsed.name)) errors.push(`Duplicate variable: ${parsed.name}`); varNames.add(parsed.name); } catch (e) { errors.push(e.message); } } return errors; } /** * Registry helper — lookup functions. */ class Registry { constructor(raw = {}) { this.raw = raw; this.services = raw.services || []; this.apis = raw.apis || []; this.components = raw.components || []; this.params = raw.params || []; this.vars = raw.vars || []; this.files = raw.files || { inputs: [], artifacts: [] }; this.docs = raw.docs || {}; this.schemas = raw.schemas || {}; } hasDoc(id) { return id in this.docs; } getDocDescription(id) { return this.docs[id] || null; } getServiceSignature(name) { const sig = this.services.find(s => s.startsWith(name + '(')); return sig ? parseServiceSignature(sig) : null; } hasComponent(id) { return this.components.includes(id); } getAPIDefinition(id) { return this.apis.find(a => a.id === id) || null; } hasAPI(id) { return this.apis.some(a => a.id === id); } getVariableDeclarations() { const map = {}; for (const v of this.vars) { try { const d = parseVariableDeclaration(v); map[d.name] = d; } catch {} } return map; } getParamDeclarations() { const map = {}; for (const p of this.params) { try { const d = parseParamDeclaration(p); map[d.name] = d; } catch {} } return map; } isInputPathAllowed(path) { return matchesAnyPattern(path, this.files.inputs || []); } isArtifactPathAllowed(path) { return matchesAnyPattern(path, this.files.artifacts || []); } hasSchema(id) { return id in this.schemas; } getSchema(id) { return this.schemas[id] || null; } } function matchesAnyPattern(path, patterns) { return patterns.some(p => matchPattern(path, p)); } function matchPattern(path, pattern) { // Convert glob to regex: * → [^/]*, ** → .* const re = pattern .replace(/[.*+?^${}()|[\]\\]/g, '\\$&') .replace(/\\\*\\\*/g, '.*') .replace(/\\\*/g, '[^/]*'); return new RegExp('^' + re + '$').test(path); } module.exports = { parseServiceSignature, parseVariableDeclaration, parseParamDeclaration, coerceParamDefault, validateRegistry, Registry };