Source

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

/**
 * @category Frontend
 * @module
 */

import {
  getRobotId,
  getRobotName,
} from '../../sessionStorage/localSsotController/ssot';

import customNotification from '../../componentsFunctionality/notificationUtils';

const FOURSPACE = '    ';

/**
 * @returns {Number} "uniqueId" which is just an increment from the counter in the local storage
 */
const getUniqueId = () => {
  const newId = JSON.parse(sessionStorage.getItem('idCounter')) + 1;
  sessionStorage.setItem('idCounter', newId);
  return newId;
};

/**
 * @returns {String} Unique Id; wrapped with the activity nomenclature
 */
const getActivityId = () => `Activity_0ay${getUniqueId()}`;

/**
 * @returns {String} Unique Id; wrapped with the event nomenclature
 */
const getEventId = () => `Event_0ay${getUniqueId()}`;

/**
 * @description Splits the robot code into an array and deletes all empty lines
 * @param {String} robotCode Code from the code editor
 * @returns {Array} Robot code without empty lines as an array
 */
const getRobotCodeAsArray = (robotCode) => {
  const robotCodeAsArray = robotCode.split('\n');
  for (let i = 0; i < robotCodeAsArray.length; i += 1) {
    if (robotCodeAsArray[i] === '') {
      robotCodeAsArray.splice(i, 1);
      i -= 1;
    }
  }
  return robotCodeAsArray;
};

/**
 * @description Checks all lines of the settings section for the right syntax and returns all declared applications as an array
 * @param {Array} robotCodeSettingsSection All lines from the settings section as an array-entry (typeof string)
 * @returns {Array} All declared applications or undefined if an error occures
 */
const getApplicationArray = (robotCodeSettingsSection) => {
  if (typeof robotCodeSettingsSection === 'undefined') return undefined;
  const robotCode = robotCodeSettingsSection.slice(1);
  const availableApplications = JSON.parse(
    sessionStorage.getItem('availableApplications')
  );
  let errorWasThrown;

  robotCode.forEach((line) => {
    const REGEX_FOR_RPA_ALIAS = /Library +RPA[.][a-zA-Z]+/;

    const elementStartsWithLibrary = line.startsWith('Library ');
    const rpaAliasIsCorrect = REGEX_FOR_RPA_ALIAS.test(line);
    const applicationIsAvailable = availableApplications.includes(
      typeof line.split('RPA.')[1] === 'undefined'
        ? ''
        : line.split('RPA.')[1].trim()
    );

    if (!elementStartsWithLibrary) {
      customNotification(
        'Error',
        `Every line of the "*** Settings ***" Section has to start with "Library"! \nError location: "${line}"`
      );
      errorWasThrown = true;
      return;
    }
    if (!rpaAliasIsCorrect) {
      customNotification(
        'Error',
        `Application has to start with "RPA." \nError location: "${line}"`
      );
      errorWasThrown = true;
      return;
    }
    if (!applicationIsAvailable) {
      customNotification(
        'Error',
        `The Application "${String(
          line.split('RPA.')[1].trim()
        )}" is currently not supported. `
      );
      errorWasThrown = true;
    }
  });

  const declaredApplications = errorWasThrown
    ? undefined
    : robotCode.map((line) => line.split('RPA.')[1].trim());

  return declaredApplications;
};

/**
 * @description Retrieves the outputVariable name from the current code line
 * @param {String} currentLine Current line of RPAf code
 * @returns {String} Name of the outputVariable
 */
const getOutputName = (currentLine) => {
  const indexOfEqualsSign = currentLine.indexOf('=');
  return currentLine
    .slice(0, indexOfEqualsSign)
    .replace('${', '')
    .replace('}', '')
    .trim();
};

/**
 * @description Retrieves the rpa task from the current code line; if there are no parameters,
 * the indexOfFirstSplitPlaceholder returns -1 and therefore the function returns the whole line
 * @param {String} currentLine Current line of RPAf code
 * @param {String} splitPlaceholder Placeholder to split the string
 * @returns {String} RpaTask for the given code line
 */
const getRpaTask = (currentLine, splitPlaceholder) => {
  const indexOfFirstSplitPlaceholder = currentLine.indexOf(splitPlaceholder);
  return indexOfFirstSplitPlaceholder === -1
    ? currentLine.replace('RPA.', '')
    : currentLine.slice(0, indexOfFirstSplitPlaceholder).replace('RPA.', '');
};

/**
 * @description Retrieves the rpa parameters from the current code line
 * @param {String} currentLine Current line of RPAf code
 * @param {String} splitPlaceholder Placeholder to split the string
 * @param {String} instructionBlocks Current intruction block to get the rpaTask
 * @returns {Array} List of parameters for the current code line
 */
const getRpaParameters = (currentLine, splitPlaceholder) => {
  const indexOfFirstSplitPlaceholder = currentLine.indexOf(splitPlaceholder);
  const parametersWithoutRpaTask = currentLine.slice(
    indexOfFirstSplitPlaceholder + splitPlaceholder.length
  );
  return parametersWithoutRpaTask.split([splitPlaceholder]);
};

/**
 * @description Deletes everything before the first occurence of '=' and then trims all emptyspace until the rpa task name to get the expected format
 * @param {String} currentLine Current line of RPAf code
 * @param {String} splitPlaceholder Placeholder to split the string
 * @returns {String} Current line without the outputVariableName prefix
 */
const currentLineWithoutOutputValueName = (completeLine, splitPlaceholder) => {
  const indexOfEqualsSign = completeLine.indexOf('=');
  let currentLine = completeLine.slice(indexOfEqualsSign + 1);
  if (currentLine.startsWith(splitPlaceholder)) {
    currentLine = currentLine.replace(splitPlaceholder, '').trim();
  } else {
    currentLine = currentLine.trim();
  }
  return currentLine;
};

/**
 * @description Counts the number of occurences of the current task in the subset
 * of all Task/Application combinations for the current robot code
 * @param {Array} allMatchingCombinations All combinations from database that match the rpaTask
 * @param {*} rpaTask RpaTask from current robotCode line
 * @returns {Number} Number of occurrences of the rpaTask in allMatchingCombinations
 */
const numberOfOccurrencesOfTask = (allMatchingCombinations, rpaTask) => {
  let numberOfOccurrences = 0;
  allMatchingCombinations.forEach((singleObject) => {
    if (singleObject.task === rpaTask) {
      numberOfOccurrences += 1;
    }
  });
  return numberOfOccurrences;
};

/**
 * @description Returns the matching task object for the rpaTask or throws a notification
 * @param {String} rpaTask RpaTask from current robotCode line
 * @param {Array} allMatchingCombinations All combinations from database that match the rpaTask
 * @returns {Object} Matching task object for the rpaTask or undefined if an error occurs
 */
const returnMatchingCombination = (rpaTask, allMatchingCombinations) => {
  const numberOfOccurrences = numberOfOccurrencesOfTask(
    allMatchingCombinations,
    rpaTask
  );

  if (allMatchingCombinations.length === 0) {
    customNotification(
      'Error',
      `The described task "${rpaTask}" could not be assigned to an application.`
    );
    return undefined;
  }
  if (numberOfOccurrences > 1) {
    let correctExampleText = '';
    allMatchingCombinations.forEach((singleCombination) => {
      correctExampleText += `\n${singleCombination.application}.${rpaTask}`;
    });
    customNotification(
      'Error',
      `Multiple Applications with task "${rpaTask}" found. Give the full name you want to use like: ${correctExampleText}`
    );
    return undefined;
  }
  return allMatchingCombinations[0];
};

/**
 * @description "Preprocesses" the code in a usable data format
 * @param {Array} robotCodeTaskSection Robot code w/o empty lines as an array of Strings
 * @param {Array} taskAndApplicationCombinations All declared tasks and applications from database
 * @returns {Array} Array of instructionBlocks with the following schema:
 *      instructionBlocks = [rpaApplication:String, rpaTask:String, name:String, paramArray:Array]
 */
const getInstructionBlocksFromTaskSection = (
  robotCodeTaskSection,
  taskAndApplicationCombinations
) => {
  let errorWasThrown;
  const instructionBlocks = [];
  const REGEX_FOR_OUTPUT_VALUE = /\${(.)+} =/;
  const SPLIT_PLACEHOLDER = '§&§';

  robotCodeTaskSection.slice(1).forEach((line) => {
    if (errorWasThrown) return;
    let currentLine = line;
    const currentLineIncludesSplitPlaceholder =
      currentLine.includes(SPLIT_PLACEHOLDER);
    const currentLineDefinesOutputValue =
      REGEX_FOR_OUTPUT_VALUE.test(currentLine);
    const currentLineStartsWithFourspace = currentLine.startsWith(FOURSPACE);

    if (!currentLineStartsWithFourspace) {
      instructionBlocks.push({ name: currentLine });
      return;
    }

    if (currentLineIncludesSplitPlaceholder) {
      customNotification(
        'Error',
        `It is not allowed to use & or § as param values \nError location: "${line}"`
      );
      errorWasThrown = true;
      return;
    }

    currentLine = currentLine.trim().replace(/( {4})/g, SPLIT_PLACEHOLDER);

    if (currentLineDefinesOutputValue) {
      const outputValueName = getOutputName(currentLine);
      instructionBlocks[instructionBlocks.length - 1].outputName =
        outputValueName;

      currentLine = currentLineWithoutOutputValueName(
        currentLine,
        SPLIT_PLACEHOLDER
      );
    }

    if (!errorWasThrown) {
      let rpaTask = getRpaTask(currentLine, SPLIT_PLACEHOLDER);
      const allMatchingCombinations = taskAndApplicationCombinations.filter(
        (singleCombination) => {
          if (rpaTask === singleCombination.task) return true;
          if (
            rpaTask.endsWith(singleCombination.task) &&
            rpaTask.startsWith(singleCombination.application)
          )
            return true;

          return false;
        }
      );

      const matchingCombination = returnMatchingCombination(
        rpaTask,
        allMatchingCombinations
      );
      if (typeof matchingCombination === 'undefined') {
        errorWasThrown = true;
        return;
      }

      rpaTask = rpaTask.replace(`${matchingCombination.application}.`, '');

      const rpaParameters = getRpaParameters(currentLine, SPLIT_PLACEHOLDER);

      instructionBlocks[instructionBlocks.length - 1].rpaTask = rpaTask;
      instructionBlocks[instructionBlocks.length - 1].paramArray =
        rpaParameters;
      instructionBlocks[instructionBlocks.length - 1].rpaApplication =
        matchingCombination.application;
    }
  });
  return errorWasThrown ? undefined : instructionBlocks;
};

/**
 * @description Builds a dummy startMarker element and returns them
 * @returns {Object} Dummy startMarker as JSON => currently MARKERS aren't defined
 * in our RPAf-Syntax and therefore there aren't implemented
 */
const buildStartMarker = () => ({
  successorIds: [],
  id: getEventId(),
  type: 'MARKER',
  name: 'START',
  predecessorIds: [],
});

/**
 * @description Builds a dummy endMarker element and returns them
 * @param {Object} predecessor As an Object to get the predecessorId
 * @returns {Object} Dummy endMarker as JSON => currently MARKERS aren't defined
 * in our RPAf-Syntax and therefore there aren't implemented
 */
const buildEndMarker = (predecessor) => ({
  successorIds: [],
  id: getEventId(),
  type: 'MARKER',
  name: 'END',
  predecessorIds: [predecessor ? predecessor.id : 'MarkerElement'],
});

/**
 * @description Builds the attributeObject for a single element
 * @param {Object} currentElement Current instruction element
 * @param {Object} singleElementFromTasksSection Parsed Object from the RPAf Code
 * @param {String} robotId Id of the current robot / ssot
 * @returns {Object} AttributeObject for a single attribute
 */
const buildSingleAttributeObject = (
  currentElement,
  singleElementFromTasksSection,
  robotId
) => {
  let { rpaTask } = singleElementFromTasksSection;
  if (!rpaTask) rpaTask = 'no Task defined';

  return {
    activityId: currentElement.id,
    rpaApplication: singleElementFromTasksSection.rpaApplication,
    rpaTask,
    robotId,
  };
};

/**
 * @description Builds the parameterObject for a single element
 * @param {Object} singleAtrributeObject Attribute Object of the current activity
 * @param {Object} singleElementFromTasksSection  Parsed Object from the RPAf Code
 * @param {Array} taskAndApplicationCombinations All combinations of applications and tasks
 * @returns {Object} ParameterObject for a single attribute
 */
const buildSingleParameterObject = (
  singleAtrributeObject,
  singleElementFromTasksSection,
  taskAndApplicationCombinations
) => {
  const { rpaApplication, activityId, rpaTask, robotId } =
    singleAtrributeObject;
  const singleParamArray = singleElementFromTasksSection.paramArray;

  const combinationObject = taskAndApplicationCombinations.filter(
    (singleCombinationObject) =>
      singleCombinationObject.application === rpaApplication &&
      singleCombinationObject.task === rpaTask
  )[0];

  const parameterArray = combinationObject.inputVars.map(
    (singleParameter, index) => {
      const currentParameterIsEmpty =
        singleParamArray[index].startsWith('%%') &&
        singleParamArray[index].endsWith('%%');
      const currentParameterRequiresUserInput =
        singleParamArray[index].startsWith('!!') &&
        singleParamArray[index].endsWith('!!');
      const currentParameterTakesOutputValue =
        singleParamArray[index].startsWith('${') &&
        singleParamArray[index].endsWith('}');
      const singleParameterObject = { ...singleParameter };

      singleParameterObject.requireUserInput =
        currentParameterRequiresUserInput;
      if (currentParameterIsEmpty || currentParameterRequiresUserInput) {
        singleParameterObject.value = '';
      } else if (currentParameterTakesOutputValue) {
        const outputValueName = singleParamArray[index]
          .slice(2)
          .slice(0, singleParamArray[index].length - 3)
          .trim();
        singleParameterObject.value = `$$${outputValueName}$$`;
      } else {
        singleParameterObject.value = singleParamArray[index];
      }
      return singleParameterObject;
    }
  );

  return {
    activityId,
    rpaParameters: parameterArray,
    robotId,
    outputValue: singleElementFromTasksSection.outputName,
  };
};

/**
 * @description Build the elementsArray of the ssot
 * @param {Array} robotCodeTaskSection Robot code w/o empty lines as an array of strings
 * @param {Array} declaredApplications All declared applications from ***settings*** section as strings
 * @param {String} robotId Id of the current robot / ssot
 * @returns {Array} elementsArray with all needed properties
 */
const getElementsArray = (
  robotCodeTaskSection,
  declaredApplications,
  robotId
) => {
  const elementsArray = [];
  const attributeArray = [];
  const parameterArray = [];

  if (
    typeof robotCodeTaskSection === 'undefined' ||
    typeof declaredApplications === 'undefined'
  )
    return undefined;

  let taskAndApplicationCombinations = JSON.parse(
    sessionStorage.getItem('taskApplicationCombinations')
  );
  taskAndApplicationCombinations = taskAndApplicationCombinations.filter(
    (singleCombination) =>
      declaredApplications.includes(singleCombination.application)
  );

  const instructionArray = getInstructionBlocksFromTaskSection(
    robotCodeTaskSection,
    taskAndApplicationCombinations
  );
  if (typeof instructionArray === 'undefined') return undefined;

  elementsArray.push(buildStartMarker());

  instructionArray.forEach((singleElement) => {
    const currentElement = {};

    currentElement.successorIds = [];
    currentElement.id = getActivityId();
    currentElement.type = 'INSTRUCTION';
    currentElement.name = singleElement.name;

    const predecessor = elementsArray[elementsArray.length - 1];
    currentElement.predecessorIds = predecessor && [predecessor.id];
    if (predecessor) predecessor.successorIds = [currentElement.id];

    elementsArray.push(currentElement);

    const singleAtrributeObject = buildSingleAttributeObject(
      currentElement,
      singleElement,
      robotId
    );
    attributeArray.push(singleAtrributeObject);

    const singleParameterObject = buildSingleParameterObject(
      singleAtrributeObject,
      singleElement,
      taskAndApplicationCombinations
    );
    parameterArray.push(singleParameterObject);
  });
  elementsArray.push(buildEndMarker(elementsArray[elementsArray.length - 1]));
  const lastElement = elementsArray[elementsArray.length - 1];
  const secontlLastElement = elementsArray[elementsArray.length - 2];
  secontlLastElement.successorIds = [lastElement.id];

  sessionStorage.removeItem('attributeLocalStorage');
  sessionStorage.setItem(
    'attributeLocalStorage',
    JSON.stringify(attributeArray)
  );
  sessionStorage.setItem(
    'parameterLocalStorage',
    JSON.stringify(parameterArray)
  );

  return elementsArray;
};

/**
 * @description Retrieves the starterId of the robot from the elements array
 * @param {Array} elementsArray Array of all elements of the robot
 * @returns {String} Id of the element that has no predecessors and is therefore the start element of the robot
 */
const getStarterId = (elementsArray) => {
  const starterElements = elementsArray.filter(
    (singleElement) =>
      singleElement.type === 'MARKER' &&
      singleElement.predecessorIds.length === 0
  );
  if (starterElements.length === 1) {
    return starterElements[0].id;
  }
  return 'no starter id found';
};

/**
 * @description Retrieves the line number for a given selector
 * @param {Array} robotCodeAsArray Complete robotCode w/o new lines as array
 * @param {String} selector Selector for which the line number will be retrieved
 * @returns {number} Line number where the selector occurs
 */
const getLineNumberForSelector = (robotCodeAsArray, selector) => {
  let lineNumber;
  robotCodeAsArray.forEach((codeLine, index) => {
    if (codeLine.trim().includes(selector)) lineNumber = index;
  });
  if (typeof lineNumber === 'undefined') {
    customNotification(
      'Error',
      `The required selector "${selector}" was not found`
    );
  }
  return lineNumber;
};

/**
 * @description Parses the RPA-Framework code from the code editor to the single source of truth
 * @param {String} robotCode Code from the code-editor
 * @returns {Object} Single source of truth as a JavaSctipt-object or undefined if an error occures
 */
const parseRobotCodeToSsot = (robotCode) => {
  const robotName = getRobotName();
  const robotId = getRobotId();
  const robotCodeAsArray = getRobotCodeAsArray(robotCode);

  const lineNumberSettingsSelector = getLineNumberForSelector(
    robotCodeAsArray,
    '*** Settings ***'
  );
  const lineNumberTasksSelector = getLineNumberForSelector(
    robotCodeAsArray,
    '*** Tasks ***'
  );

  let robotCodeSettingsSection;
  let robotCodeTaskSection;
  if (
    typeof lineNumberSettingsSelector !== 'undefined' &&
    typeof lineNumberTasksSelector !== 'undefined'
  ) {
    robotCodeSettingsSection = robotCodeAsArray.slice(
      lineNumberSettingsSelector,
      lineNumberTasksSelector
    );
    robotCodeTaskSection = robotCodeAsArray.slice(lineNumberTasksSelector);
  }

  const declaredApplications = getApplicationArray(robotCodeSettingsSection);
  const elementsArray = getElementsArray(
    robotCodeTaskSection,
    declaredApplications,
    robotId
  );

  if (typeof elementsArray !== 'undefined') {
    const ssot = {
      _id: robotId,
      starterId: getStarterId(elementsArray),
      robotName,
      elements: elementsArray,
    };
    return ssot;
  }
  return undefined;
};

export {
  parseRobotCodeToSsot,
  getLineNumberForSelector,
  getRobotCodeAsArray,
  getApplicationArray,
  getElementsArray,
  getInstructionBlocksFromTaskSection,
};