| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219 |
- /**
- * 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
- };
|