Source

frontend/src/utils/parser/robotCodeToSsotParsing/robotCodeToSsotParsing.js

  1. /**
  2. * @category Frontend
  3. * @module
  4. */
  5. import {
  6. getRobotId,
  7. getRobotName,
  8. } from '../../sessionStorage/localSsotController/ssot';
  9. import customNotification from '../../componentsFunctionality/notificationUtils';
  10. const FOURSPACE = ' ';
  11. /**
  12. * @returns {Number} "uniqueId" which is just an increment from the counter in the local storage
  13. */
  14. const getUniqueId = () => {
  15. const newId = JSON.parse(sessionStorage.getItem('idCounter')) + 1;
  16. sessionStorage.setItem('idCounter', newId);
  17. return newId;
  18. };
  19. /**
  20. * @returns {String} Unique Id; wrapped with the activity nomenclature
  21. */
  22. const getActivityId = () => `Activity_0ay${getUniqueId()}`;
  23. /**
  24. * @returns {String} Unique Id; wrapped with the event nomenclature
  25. */
  26. const getEventId = () => `Event_0ay${getUniqueId()}`;
  27. /**
  28. * @description Splits the robot code into an array and deletes all empty lines
  29. * @param {String} robotCode Code from the code editor
  30. * @returns {Array} Robot code without empty lines as an array
  31. */
  32. const getRobotCodeAsArray = (robotCode) => {
  33. const robotCodeAsArray = robotCode.split('\n');
  34. for (let i = 0; i < robotCodeAsArray.length; i += 1) {
  35. if (robotCodeAsArray[i] === '') {
  36. robotCodeAsArray.splice(i, 1);
  37. i -= 1;
  38. }
  39. }
  40. return robotCodeAsArray;
  41. };
  42. /**
  43. * @description Checks all lines of the settings section for the right syntax and returns all declared applications as an array
  44. * @param {Array} robotCodeSettingsSection All lines from the settings section as an array-entry (typeof string)
  45. * @returns {Array} All declared applications or undefined if an error occures
  46. */
  47. const getApplicationArray = (robotCodeSettingsSection) => {
  48. if (typeof robotCodeSettingsSection === 'undefined') return undefined;
  49. const robotCode = robotCodeSettingsSection.slice(1);
  50. const availableApplications = JSON.parse(
  51. sessionStorage.getItem('availableApplications')
  52. );
  53. let errorWasThrown;
  54. robotCode.forEach((line) => {
  55. const REGEX_FOR_RPA_ALIAS = /Library +RPA[.][a-zA-Z]+/;
  56. const elementStartsWithLibrary = line.startsWith('Library ');
  57. const rpaAliasIsCorrect = REGEX_FOR_RPA_ALIAS.test(line);
  58. const applicationIsAvailable = availableApplications.includes(
  59. typeof line.split('RPA.')[1] === 'undefined'
  60. ? ''
  61. : line.split('RPA.')[1].trim()
  62. );
  63. if (!elementStartsWithLibrary) {
  64. customNotification(
  65. 'Error',
  66. `Every line of the "*** Settings ***" Section has to start with "Library"! \nError location: "${line}"`
  67. );
  68. errorWasThrown = true;
  69. return;
  70. }
  71. if (!rpaAliasIsCorrect) {
  72. customNotification(
  73. 'Error',
  74. `Application has to start with "RPA." \nError location: "${line}"`
  75. );
  76. errorWasThrown = true;
  77. return;
  78. }
  79. if (!applicationIsAvailable) {
  80. customNotification(
  81. 'Error',
  82. `The Application "${String(
  83. line.split('RPA.')[1].trim()
  84. )}" is currently not supported. `
  85. );
  86. errorWasThrown = true;
  87. }
  88. });
  89. const declaredApplications = errorWasThrown
  90. ? undefined
  91. : robotCode.map((line) => line.split('RPA.')[1].trim());
  92. return declaredApplications;
  93. };
  94. /**
  95. * @description Retrieves the outputVariable name from the current code line
  96. * @param {String} currentLine Current line of RPAf code
  97. * @returns {String} Name of the outputVariable
  98. */
  99. const getOutputName = (currentLine) => {
  100. const indexOfEqualsSign = currentLine.indexOf('=');
  101. return currentLine
  102. .slice(0, indexOfEqualsSign)
  103. .replace('${', '')
  104. .replace('}', '')
  105. .trim();
  106. };
  107. /**
  108. * @description Retrieves the rpa task from the current code line; if there are no parameters,
  109. * the indexOfFirstSplitPlaceholder returns -1 and therefore the function returns the whole line
  110. * @param {String} currentLine Current line of RPAf code
  111. * @param {String} splitPlaceholder Placeholder to split the string
  112. * @returns {String} RpaTask for the given code line
  113. */
  114. const getRpaTask = (currentLine, splitPlaceholder) => {
  115. const indexOfFirstSplitPlaceholder = currentLine.indexOf(splitPlaceholder);
  116. return indexOfFirstSplitPlaceholder === -1
  117. ? currentLine.replace('RPA.', '')
  118. : currentLine.slice(0, indexOfFirstSplitPlaceholder).replace('RPA.', '');
  119. };
  120. /**
  121. * @description Retrieves the rpa parameters from the current code line
  122. * @param {String} currentLine Current line of RPAf code
  123. * @param {String} splitPlaceholder Placeholder to split the string
  124. * @param {String} instructionBlocks Current intruction block to get the rpaTask
  125. * @returns {Array} List of parameters for the current code line
  126. */
  127. const getRpaParameters = (currentLine, splitPlaceholder) => {
  128. const indexOfFirstSplitPlaceholder = currentLine.indexOf(splitPlaceholder);
  129. const parametersWithoutRpaTask = currentLine.slice(
  130. indexOfFirstSplitPlaceholder + splitPlaceholder.length
  131. );
  132. return parametersWithoutRpaTask.split([splitPlaceholder]);
  133. };
  134. /**
  135. * @description Deletes everything before the first occurence of '=' and then trims all emptyspace until the rpa task name to get the expected format
  136. * @param {String} currentLine Current line of RPAf code
  137. * @param {String} splitPlaceholder Placeholder to split the string
  138. * @returns {String} Current line without the outputVariableName prefix
  139. */
  140. const currentLineWithoutOutputValueName = (completeLine, splitPlaceholder) => {
  141. const indexOfEqualsSign = completeLine.indexOf('=');
  142. let currentLine = completeLine.slice(indexOfEqualsSign + 1);
  143. if (currentLine.startsWith(splitPlaceholder)) {
  144. currentLine = currentLine.replace(splitPlaceholder, '').trim();
  145. } else {
  146. currentLine = currentLine.trim();
  147. }
  148. return currentLine;
  149. };
  150. /**
  151. * @description Counts the number of occurences of the current task in the subset
  152. * of all Task/Application combinations for the current robot code
  153. * @param {Array} allMatchingCombinations All combinations from database that match the rpaTask
  154. * @param {*} rpaTask RpaTask from current robotCode line
  155. * @returns {Number} Number of occurrences of the rpaTask in allMatchingCombinations
  156. */
  157. const numberOfOccurrencesOfTask = (allMatchingCombinations, rpaTask) => {
  158. let numberOfOccurrences = 0;
  159. allMatchingCombinations.forEach((singleObject) => {
  160. if (singleObject.task === rpaTask) {
  161. numberOfOccurrences += 1;
  162. }
  163. });
  164. return numberOfOccurrences;
  165. };
  166. /**
  167. * @description Returns the matching task object for the rpaTask or throws a notification
  168. * @param {String} rpaTask RpaTask from current robotCode line
  169. * @param {Array} allMatchingCombinations All combinations from database that match the rpaTask
  170. * @returns {Object} Matching task object for the rpaTask or undefined if an error occurs
  171. */
  172. const returnMatchingCombination = (rpaTask, allMatchingCombinations) => {
  173. const numberOfOccurrences = numberOfOccurrencesOfTask(
  174. allMatchingCombinations,
  175. rpaTask
  176. );
  177. if (allMatchingCombinations.length === 0) {
  178. customNotification(
  179. 'Error',
  180. `The described task "${rpaTask}" could not be assigned to an application.`
  181. );
  182. return undefined;
  183. }
  184. if (numberOfOccurrences > 1) {
  185. let correctExampleText = '';
  186. allMatchingCombinations.forEach((singleCombination) => {
  187. correctExampleText += `\n${singleCombination.application}.${rpaTask}`;
  188. });
  189. customNotification(
  190. 'Error',
  191. `Multiple Applications with task "${rpaTask}" found. Give the full name you want to use like: ${correctExampleText}`
  192. );
  193. return undefined;
  194. }
  195. return allMatchingCombinations[0];
  196. };
  197. /**
  198. * @description "Preprocesses" the code in a usable data format
  199. * @param {Array} robotCodeTaskSection Robot code w/o empty lines as an array of Strings
  200. * @param {Array} taskAndApplicationCombinations All declared tasks and applications from database
  201. * @returns {Array} Array of instructionBlocks with the following schema:
  202. * instructionBlocks = [rpaApplication:String, rpaTask:String, name:String, paramArray:Array]
  203. */
  204. const getInstructionBlocksFromTaskSection = (
  205. robotCodeTaskSection,
  206. taskAndApplicationCombinations
  207. ) => {
  208. let errorWasThrown;
  209. const instructionBlocks = [];
  210. const REGEX_FOR_OUTPUT_VALUE = /\${(.)+} =/;
  211. const SPLIT_PLACEHOLDER = '§&§';
  212. robotCodeTaskSection.slice(1).forEach((line) => {
  213. if (errorWasThrown) return;
  214. let currentLine = line;
  215. const currentLineIncludesSplitPlaceholder =
  216. currentLine.includes(SPLIT_PLACEHOLDER);
  217. const currentLineDefinesOutputValue =
  218. REGEX_FOR_OUTPUT_VALUE.test(currentLine);
  219. const currentLineStartsWithFourspace = currentLine.startsWith(FOURSPACE);
  220. if (!currentLineStartsWithFourspace) {
  221. instructionBlocks.push({ name: currentLine });
  222. return;
  223. }
  224. if (currentLineIncludesSplitPlaceholder) {
  225. customNotification(
  226. 'Error',
  227. `It is not allowed to use & or § as param values \nError location: "${line}"`
  228. );
  229. errorWasThrown = true;
  230. return;
  231. }
  232. currentLine = currentLine.trim().replace(/( {4})/g, SPLIT_PLACEHOLDER);
  233. if (currentLineDefinesOutputValue) {
  234. const outputValueName = getOutputName(currentLine);
  235. instructionBlocks[instructionBlocks.length - 1].outputName =
  236. outputValueName;
  237. currentLine = currentLineWithoutOutputValueName(
  238. currentLine,
  239. SPLIT_PLACEHOLDER
  240. );
  241. }
  242. if (!errorWasThrown) {
  243. let rpaTask = getRpaTask(currentLine, SPLIT_PLACEHOLDER);
  244. const allMatchingCombinations = taskAndApplicationCombinations.filter(
  245. (singleCombination) => {
  246. if (rpaTask === singleCombination.task) return true;
  247. if (
  248. rpaTask.endsWith(singleCombination.task) &&
  249. rpaTask.startsWith(singleCombination.application)
  250. )
  251. return true;
  252. return false;
  253. }
  254. );
  255. const matchingCombination = returnMatchingCombination(
  256. rpaTask,
  257. allMatchingCombinations
  258. );
  259. if (typeof matchingCombination === 'undefined') {
  260. errorWasThrown = true;
  261. return;
  262. }
  263. rpaTask = rpaTask.replace(`${matchingCombination.application}.`, '');
  264. const rpaParameters = getRpaParameters(currentLine, SPLIT_PLACEHOLDER);
  265. instructionBlocks[instructionBlocks.length - 1].rpaTask = rpaTask;
  266. instructionBlocks[instructionBlocks.length - 1].paramArray =
  267. rpaParameters;
  268. instructionBlocks[instructionBlocks.length - 1].rpaApplication =
  269. matchingCombination.application;
  270. }
  271. });
  272. return errorWasThrown ? undefined : instructionBlocks;
  273. };
  274. /**
  275. * @description Builds a dummy startMarker element and returns them
  276. * @returns {Object} Dummy startMarker as JSON => currently MARKERS aren't defined
  277. * in our RPAf-Syntax and therefore there aren't implemented
  278. */
  279. const buildStartMarker = () => ({
  280. successorIds: [],
  281. id: getEventId(),
  282. type: 'MARKER',
  283. name: 'START',
  284. predecessorIds: [],
  285. });
  286. /**
  287. * @description Builds a dummy endMarker element and returns them
  288. * @param {Object} predecessor As an Object to get the predecessorId
  289. * @returns {Object} Dummy endMarker as JSON => currently MARKERS aren't defined
  290. * in our RPAf-Syntax and therefore there aren't implemented
  291. */
  292. const buildEndMarker = (predecessor) => ({
  293. successorIds: [],
  294. id: getEventId(),
  295. type: 'MARKER',
  296. name: 'END',
  297. predecessorIds: [predecessor ? predecessor.id : 'MarkerElement'],
  298. });
  299. /**
  300. * @description Builds the attributeObject for a single element
  301. * @param {Object} currentElement Current instruction element
  302. * @param {Object} singleElementFromTasksSection Parsed Object from the RPAf Code
  303. * @param {String} robotId Id of the current robot / ssot
  304. * @returns {Object} AttributeObject for a single attribute
  305. */
  306. const buildSingleAttributeObject = (
  307. currentElement,
  308. singleElementFromTasksSection,
  309. robotId
  310. ) => {
  311. let { rpaTask } = singleElementFromTasksSection;
  312. if (!rpaTask) rpaTask = 'no Task defined';
  313. return {
  314. activityId: currentElement.id,
  315. rpaApplication: singleElementFromTasksSection.rpaApplication,
  316. rpaTask,
  317. robotId,
  318. };
  319. };
  320. /**
  321. * @description Builds the parameterObject for a single element
  322. * @param {Object} singleAtrributeObject Attribute Object of the current activity
  323. * @param {Object} singleElementFromTasksSection Parsed Object from the RPAf Code
  324. * @param {Array} taskAndApplicationCombinations All combinations of applications and tasks
  325. * @returns {Object} ParameterObject for a single attribute
  326. */
  327. const buildSingleParameterObject = (
  328. singleAtrributeObject,
  329. singleElementFromTasksSection,
  330. taskAndApplicationCombinations
  331. ) => {
  332. const { rpaApplication, activityId, rpaTask, robotId } =
  333. singleAtrributeObject;
  334. const singleParamArray = singleElementFromTasksSection.paramArray;
  335. const combinationObject = taskAndApplicationCombinations.filter(
  336. (singleCombinationObject) =>
  337. singleCombinationObject.application === rpaApplication &&
  338. singleCombinationObject.task === rpaTask
  339. )[0];
  340. const parameterArray = combinationObject.inputVars.map(
  341. (singleParameter, index) => {
  342. const currentParameterIsEmpty =
  343. singleParamArray[index].startsWith('%%') &&
  344. singleParamArray[index].endsWith('%%');
  345. const currentParameterRequiresUserInput =
  346. singleParamArray[index].startsWith('!!') &&
  347. singleParamArray[index].endsWith('!!');
  348. const currentParameterTakesOutputValue =
  349. singleParamArray[index].startsWith('${') &&
  350. singleParamArray[index].endsWith('}');
  351. const singleParameterObject = { ...singleParameter };
  352. singleParameterObject.requireUserInput =
  353. currentParameterRequiresUserInput;
  354. if (currentParameterIsEmpty || currentParameterRequiresUserInput) {
  355. singleParameterObject.value = '';
  356. } else if (currentParameterTakesOutputValue) {
  357. const outputValueName = singleParamArray[index]
  358. .slice(2)
  359. .slice(0, singleParamArray[index].length - 3)
  360. .trim();
  361. singleParameterObject.value = `$$${outputValueName}$$`;
  362. } else {
  363. singleParameterObject.value = singleParamArray[index];
  364. }
  365. return singleParameterObject;
  366. }
  367. );
  368. return {
  369. activityId,
  370. rpaParameters: parameterArray,
  371. robotId,
  372. outputValue: singleElementFromTasksSection.outputName,
  373. };
  374. };
  375. /**
  376. * @description Build the elementsArray of the ssot
  377. * @param {Array} robotCodeTaskSection Robot code w/o empty lines as an array of strings
  378. * @param {Array} declaredApplications All declared applications from ***settings*** section as strings
  379. * @param {String} robotId Id of the current robot / ssot
  380. * @returns {Array} elementsArray with all needed properties
  381. */
  382. const getElementsArray = (
  383. robotCodeTaskSection,
  384. declaredApplications,
  385. robotId
  386. ) => {
  387. const elementsArray = [];
  388. const attributeArray = [];
  389. const parameterArray = [];
  390. if (
  391. typeof robotCodeTaskSection === 'undefined' ||
  392. typeof declaredApplications === 'undefined'
  393. )
  394. return undefined;
  395. let taskAndApplicationCombinations = JSON.parse(
  396. sessionStorage.getItem('taskApplicationCombinations')
  397. );
  398. taskAndApplicationCombinations = taskAndApplicationCombinations.filter(
  399. (singleCombination) =>
  400. declaredApplications.includes(singleCombination.application)
  401. );
  402. const instructionArray = getInstructionBlocksFromTaskSection(
  403. robotCodeTaskSection,
  404. taskAndApplicationCombinations
  405. );
  406. if (typeof instructionArray === 'undefined') return undefined;
  407. elementsArray.push(buildStartMarker());
  408. instructionArray.forEach((singleElement) => {
  409. const currentElement = {};
  410. currentElement.successorIds = [];
  411. currentElement.id = getActivityId();
  412. currentElement.type = 'INSTRUCTION';
  413. currentElement.name = singleElement.name;
  414. const predecessor = elementsArray[elementsArray.length - 1];
  415. currentElement.predecessorIds = predecessor && [predecessor.id];
  416. if (predecessor) predecessor.successorIds = [currentElement.id];
  417. elementsArray.push(currentElement);
  418. const singleAtrributeObject = buildSingleAttributeObject(
  419. currentElement,
  420. singleElement,
  421. robotId
  422. );
  423. attributeArray.push(singleAtrributeObject);
  424. const singleParameterObject = buildSingleParameterObject(
  425. singleAtrributeObject,
  426. singleElement,
  427. taskAndApplicationCombinations
  428. );
  429. parameterArray.push(singleParameterObject);
  430. });
  431. elementsArray.push(buildEndMarker(elementsArray[elementsArray.length - 1]));
  432. const lastElement = elementsArray[elementsArray.length - 1];
  433. const secontlLastElement = elementsArray[elementsArray.length - 2];
  434. secontlLastElement.successorIds = [lastElement.id];
  435. sessionStorage.removeItem('attributeLocalStorage');
  436. sessionStorage.setItem(
  437. 'attributeLocalStorage',
  438. JSON.stringify(attributeArray)
  439. );
  440. sessionStorage.setItem(
  441. 'parameterLocalStorage',
  442. JSON.stringify(parameterArray)
  443. );
  444. return elementsArray;
  445. };
  446. /**
  447. * @description Retrieves the starterId of the robot from the elements array
  448. * @param {Array} elementsArray Array of all elements of the robot
  449. * @returns {String} Id of the element that has no predecessors and is therefore the start element of the robot
  450. */
  451. const getStarterId = (elementsArray) => {
  452. const starterElements = elementsArray.filter(
  453. (singleElement) =>
  454. singleElement.type === 'MARKER' &&
  455. singleElement.predecessorIds.length === 0
  456. );
  457. if (starterElements.length === 1) {
  458. return starterElements[0].id;
  459. }
  460. return 'no starter id found';
  461. };
  462. /**
  463. * @description Retrieves the line number for a given selector
  464. * @param {Array} robotCodeAsArray Complete robotCode w/o new lines as array
  465. * @param {String} selector Selector for which the line number will be retrieved
  466. * @returns {number} Line number where the selector occurs
  467. */
  468. const getLineNumberForSelector = (robotCodeAsArray, selector) => {
  469. let lineNumber;
  470. robotCodeAsArray.forEach((codeLine, index) => {
  471. if (codeLine.trim().includes(selector)) lineNumber = index;
  472. });
  473. if (typeof lineNumber === 'undefined') {
  474. customNotification(
  475. 'Error',
  476. `The required selector "${selector}" was not found`
  477. );
  478. }
  479. return lineNumber;
  480. };
  481. /**
  482. * @description Parses the RPA-Framework code from the code editor to the single source of truth
  483. * @param {String} robotCode Code from the code-editor
  484. * @returns {Object} Single source of truth as a JavaSctipt-object or undefined if an error occures
  485. */
  486. const parseRobotCodeToSsot = (robotCode) => {
  487. const robotName = getRobotName();
  488. const robotId = getRobotId();
  489. const robotCodeAsArray = getRobotCodeAsArray(robotCode);
  490. const lineNumberSettingsSelector = getLineNumberForSelector(
  491. robotCodeAsArray,
  492. '*** Settings ***'
  493. );
  494. const lineNumberTasksSelector = getLineNumberForSelector(
  495. robotCodeAsArray,
  496. '*** Tasks ***'
  497. );
  498. let robotCodeSettingsSection;
  499. let robotCodeTaskSection;
  500. if (
  501. typeof lineNumberSettingsSelector !== 'undefined' &&
  502. typeof lineNumberTasksSelector !== 'undefined'
  503. ) {
  504. robotCodeSettingsSection = robotCodeAsArray.slice(
  505. lineNumberSettingsSelector,
  506. lineNumberTasksSelector
  507. );
  508. robotCodeTaskSection = robotCodeAsArray.slice(lineNumberTasksSelector);
  509. }
  510. const declaredApplications = getApplicationArray(robotCodeSettingsSection);
  511. const elementsArray = getElementsArray(
  512. robotCodeTaskSection,
  513. declaredApplications,
  514. robotId
  515. );
  516. if (typeof elementsArray !== 'undefined') {
  517. const ssot = {
  518. _id: robotId,
  519. starterId: getStarterId(elementsArray),
  520. robotName,
  521. elements: elementsArray,
  522. };
  523. return ssot;
  524. }
  525. return undefined;
  526. };
  527. export {
  528. parseRobotCodeToSsot,
  529. getLineNumberForSelector,
  530. getRobotCodeAsArray,
  531. getApplicationArray,
  532. getElementsArray,
  533. getInstructionBlocksFromTaskSection,
  534. };