registry.js 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219
  1. /**
  2. * VL Workflow Engine — Registry Parser & Validator
  3. * Ported from Go: workflow/registry.go
  4. * Spec: v3.15
  5. */
  6. /**
  7. * Parse a service signature:
  8. * "ServiceName(param1(TYPE1), param2(TYPE2)) RETURN result1(TYPE1)"
  9. */
  10. function parseServiceSignature(sig) {
  11. // Handle nested parens: ServiceName(param1(TYPE1), param2(TYPE2)) RETURN ...
  12. const nameEnd = sig.indexOf('(');
  13. if (nameEnd < 0) throw new Error(`Invalid service signature: ${sig}`);
  14. const name = sig.slice(0, nameEnd).trim();
  15. // Find the matching close paren for the outer params
  16. let depth = 0, paramsEnd = -1;
  17. for (let i = nameEnd; i < sig.length; i++) {
  18. if (sig[i] === '(') depth++;
  19. else if (sig[i] === ')') { depth--; if (depth === 0) { paramsEnd = i; break; } }
  20. }
  21. if (paramsEnd < 0) throw new Error(`Invalid service signature: ${sig}`);
  22. const paramsStr = sig.slice(nameEnd + 1, paramsEnd);
  23. const rest = sig.slice(paramsEnd + 1).trim();
  24. const returnsStr = rest.startsWith('RETURN') ? rest.slice(6).trim() : '';
  25. return {
  26. name,
  27. parameters: paramsStr ? parseNestedParamList(paramsStr) : [],
  28. returns: returnsStr ? parseNestedParamList(returnsStr) : []
  29. };
  30. }
  31. function parseParamList(str) {
  32. return str.split(',').map(s => s.trim()).filter(Boolean).map(s => {
  33. const m = s.match(/^(\w+)\(([^)]+)\)$/);
  34. return m ? { name: m[1], type: m[2] } : { name: s, type: 'STRING' };
  35. });
  36. }
  37. /** Split nested param list respecting parens: "a(TYPE), b(TYPE)" */
  38. function parseNestedParamList(str) {
  39. const items = [];
  40. let depth = 0, start = 0;
  41. for (let i = 0; i <= str.length; i++) {
  42. if (i === str.length || (str[i] === ',' && depth === 0)) {
  43. const item = str.slice(start, i).trim();
  44. if (item) {
  45. const m = item.match(/^(\w+)\((.+)\)$/);
  46. items.push(m ? { name: m[1], type: m[2] } : { name: item, type: 'STRING' });
  47. }
  48. start = i + 1;
  49. } else if (str[i] === '(') depth++;
  50. else if (str[i] === ')') depth--;
  51. }
  52. return items;
  53. }
  54. /**
  55. * Parse a variable declaration: "$varName(TYPE)" or "$varName([TYPE])"
  56. */
  57. function parseVariableDeclaration(decl) {
  58. const m = decl.match(/^(\$\w+)\((\[?\w+\]?)\)$/);
  59. if (!m) throw new Error(`Invalid variable declaration: ${decl}`);
  60. return { name: m[1], type: m[2] };
  61. }
  62. /**
  63. * Parse a param declaration: "paramName(TYPE)" or "paramName(TYPE) = default"
  64. */
  65. function parseParamDeclaration(decl) {
  66. const m = decl.match(/^(\w+)\((\w+)\)(?:\s*=\s*(.+))?$/);
  67. if (!m) throw new Error(`Invalid param declaration: ${decl}`);
  68. const result = { name: m[1], type: m[2], default: undefined };
  69. if (m[3] !== undefined) {
  70. let def = m[3].trim();
  71. // Strip surrounding quotes
  72. if ((def.startsWith('"') && def.endsWith('"')) || (def.startsWith("'") && def.endsWith("'"))) {
  73. def = def.slice(1, -1);
  74. }
  75. result.default = coerceParamDefault(result.type, def);
  76. }
  77. return result;
  78. }
  79. function coerceParamDefault(type, val) {
  80. switch (type) {
  81. case 'INT': { const n = parseInt(val, 10); return isNaN(n) ? val : n; }
  82. case 'FLOAT': { const n = parseFloat(val); return isNaN(n) ? val : n; }
  83. case 'BOOL': return val === 'true' || val === '1';
  84. default: return val;
  85. }
  86. }
  87. /**
  88. * Validate a registry object.
  89. */
  90. function validateRegistry(registry) {
  91. const errors = [];
  92. if (!registry) return errors;
  93. // Duplicate service names
  94. const svcNames = new Set();
  95. for (const sig of (registry.services || [])) {
  96. try {
  97. const parsed = parseServiceSignature(sig);
  98. if (svcNames.has(parsed.name)) errors.push(`Duplicate service: ${parsed.name}`);
  99. svcNames.add(parsed.name);
  100. } catch (e) { errors.push(e.message); }
  101. }
  102. // Duplicate API IDs
  103. const apiIds = new Set();
  104. for (const api of (registry.apis || [])) {
  105. if (!api.id) errors.push('API missing id');
  106. else if (apiIds.has(api.id)) errors.push(`Duplicate API: ${api.id}`);
  107. else apiIds.add(api.id);
  108. if (!api.url) errors.push(`API ${api.id}: url required`);
  109. if (api.method && !['GET','POST','PUT','PATCH','DELETE'].includes(api.method.toUpperCase())) {
  110. errors.push(`API ${api.id}: invalid method ${api.method}`);
  111. }
  112. }
  113. // Duplicate params
  114. const paramNames = new Set();
  115. for (const p of (registry.params || [])) {
  116. try {
  117. const parsed = parseParamDeclaration(p);
  118. if (paramNames.has(parsed.name)) errors.push(`Duplicate param: ${parsed.name}`);
  119. paramNames.add(parsed.name);
  120. } catch (e) { errors.push(e.message); }
  121. }
  122. // Duplicate vars
  123. const varNames = new Set();
  124. for (const v of (registry.vars || [])) {
  125. try {
  126. const parsed = parseVariableDeclaration(v);
  127. if (varNames.has(parsed.name)) errors.push(`Duplicate variable: ${parsed.name}`);
  128. varNames.add(parsed.name);
  129. } catch (e) { errors.push(e.message); }
  130. }
  131. return errors;
  132. }
  133. /**
  134. * Registry helper — lookup functions.
  135. */
  136. class Registry {
  137. constructor(raw = {}) {
  138. this.raw = raw;
  139. this.services = raw.services || [];
  140. this.apis = raw.apis || [];
  141. this.components = raw.components || [];
  142. this.params = raw.params || [];
  143. this.vars = raw.vars || [];
  144. this.files = raw.files || { inputs: [], artifacts: [] };
  145. this.docs = raw.docs || {};
  146. this.schemas = raw.schemas || {};
  147. }
  148. hasDoc(id) { return id in this.docs; }
  149. getDocDescription(id) { return this.docs[id] || null; }
  150. getServiceSignature(name) {
  151. const sig = this.services.find(s => s.startsWith(name + '('));
  152. return sig ? parseServiceSignature(sig) : null;
  153. }
  154. hasComponent(id) { return this.components.includes(id); }
  155. getAPIDefinition(id) {
  156. return this.apis.find(a => a.id === id) || null;
  157. }
  158. hasAPI(id) { return this.apis.some(a => a.id === id); }
  159. getVariableDeclarations() {
  160. const map = {};
  161. for (const v of this.vars) {
  162. try { const d = parseVariableDeclaration(v); map[d.name] = d; } catch {}
  163. }
  164. return map;
  165. }
  166. getParamDeclarations() {
  167. const map = {};
  168. for (const p of this.params) {
  169. try { const d = parseParamDeclaration(p); map[d.name] = d; } catch {}
  170. }
  171. return map;
  172. }
  173. isInputPathAllowed(path) { return matchesAnyPattern(path, this.files.inputs || []); }
  174. isArtifactPathAllowed(path) { return matchesAnyPattern(path, this.files.artifacts || []); }
  175. hasSchema(id) { return id in this.schemas; }
  176. getSchema(id) { return this.schemas[id] || null; }
  177. }
  178. function matchesAnyPattern(path, patterns) {
  179. return patterns.some(p => matchPattern(path, p));
  180. }
  181. function matchPattern(path, pattern) {
  182. // Convert glob to regex: * → [^/]*, ** → .*
  183. const re = pattern
  184. .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  185. .replace(/\\\*\\\*/g, '.*')
  186. .replace(/\\\*/g, '[^/]*');
  187. return new RegExp('^' + re + '$').test(path);
  188. }
  189. module.exports = {
  190. parseServiceSignature, parseVariableDeclaration, parseParamDeclaration,
  191. coerceParamDefault, validateRegistry, Registry
  192. };