Source: engine/universal/capabilities/parser/parameterAndOutputParser.js

const _ = require('lodash');

const { NotFoundError } = require('../error/notFoundError');
const parseHttpAndReplaceHash = require('./urlParser');
const extractValue = require('./objectParser/extractValue');
const mergeRecursively = require('./objectParser/insertAndMergeRecursively');
const objectDeepKeys = require('./objectParser/objectDeepKeys');
const findObjectByKeyVal = require('./objectParser/findObjectByKeyVal');
const replaceKeysOfReturnObject = require('./objectParser/replaceKeysOfReturnObject');
const validateExpectedParametersNode = require('./validator/validateExpectedParametersNode');
const getParameters = require('./objectParser/getParameters');
const validateParameterDesc = require('./validator/validateParameterDescriptions');
const validateParameterMapping = require('./validator/validateParameterMapping');
const validateOutputMapping = require('./validator/validateOutputMapping');

const parameterUri = 'https://w3id.org/function/ontology#Parameter';
const functionParamMappingUri = 'https://w3id.org/function/vocabulary/mapping#functionParameter';
const predicateUri = 'https://w3id.org/function/ontology#predicate';
const hasPartUri = 'http://purl.org/dc/terms/hasPart';
const implementationUri = 'https://w3id.org/function/vocabulary/mapping#implementationProperty';
const requiredUri = 'https://w3id.org/function/ontology#required';
const unitTextUri = 'https://schema.org/unitText';
const encodingFormatUri = 'https://schema.org/encodingFormat';
const minValueUri = 'https://schema.org/minValue';
const maxValueUri = 'https://schema.org/maxValue';
const mappingUri = 'https://w3id.org/function/ontology#Mapping';
const returnMappingUri = 'https://w3id.org/function/ontology#returnMapping';
const outputUri = 'https://w3id.org/function/ontology#Output';
const expectedArgumentUri = 'https://w3id.org/function/ontology#expects';
const paramMappingUri = 'https://w3id.org/function/ontology#parameterMapping';

/**
 * @module parser
 * @memberof module:@proceed/capabilities
 */

/**
 * @module parameterAndOutputParser
 * @memberof module:@proceed/capabilities.module:parser
 */

/**
  A Function that parses the URI do its domain and actions form
  @param {object} parameterMapping
  @param {object} expectedArg
  @returns {object} corresponding function parameter mapping of the expected
  argument
*/
function findMapping(parameterMapping, expectedArg) {
  const mapping = parameterMapping.find((i) => {
    let fParam = i[functionParamMappingUri];
    if (typeof fParam === 'object' && fParam.length === 1) {
      fParam = fParam.find((e) => e['@value'])['@value'];
    } else {
      return false;
    }
    return fParam === expectedArg['@id'];
  });
  return mapping;
}

/**
  A function that matches the expected arguments with the given arguments using
  parameter mapping und parameter description in expanded jsonld the semantic description
  If there is a unit or encoding Format is given, the check is also made here.
  @param {string} expectedArg given in the semantic description of the capabilities
  @param {array} parameterMapping the expanded jsonld node of parameterMapping in the
  semantic description where
  the parameter description and native function parameter are mapped
  @param {array} parameterDescriptions the expanded jsonld semantic description node
  of the parameter
   @returns {list} the native function parameter and the value
*/
function parseParamRecursively(expectedArg, parameterMapping, parameterDescriptions, args) {
  if (args === undefined) {
    return [];
  }
  const parsedArgs = {};
  for (const key of Object.keys(args)) {
    if (key.startsWith('http')) {
      parsedArgs[parseHttpAndReplaceHash(key)] = args[key];
    } else {
      parsedArgs[key] = args[key];
    }
  }
  const mapping = findMapping(parameterMapping, expectedArg);
  const paramDescription = parameterDescriptions.find(
    (i) => i['@id'] === expectedArg['@id'] || i['@id'] === expectedArg
  );
  const predicate = getParameters.getPredicateForParam(
    paramDescription,
    parameterUri,
    predicateUri
  );
  let value = extractValue(parsedArgs, predicate);
  if (paramDescription[hasPartUri] !== undefined) {
    let collectedValues = [];
    const childParamMappings = parameterMapping
      .filter((p) => {
        const paramName = p[functionParamMappingUri];
        return paramName[0]['@value'] === expectedArg['@id'];
      })
      .map((p) => ({
        ...p,
        'https://w3id.org/function/vocabulary/mapping#functionParameter':
          p[functionParamMappingUri].slice(1),
      }));
    const objectGraph = paramDescription[hasPartUri].find((e) => e['@list'])['@list'];
    for (const childParam of objectGraph) {
      collectedValues = collectedValues.concat(
        parseParamRecursively(childParam, childParamMappings, objectGraph, value)
      );
    }
    return collectedValues;
  }
  if (paramDescription[requiredUri][0]['@value'] && value === undefined) {
    throw new NotFoundError(
      'Required parameters should be included in the startCapability function'
    );
  }
  if (value !== undefined) {
    if (paramDescription[unitTextUri] !== undefined && value.unit !== undefined) {
      if (paramDescription[unitTextUri][0]['@value'] !== value.unit) {
        throw new NotFoundError(
          'The unit of the parameter is not correct, please check the semantic description!'
        );
      }
    }
    if (value.unit !== undefined) {
      // eslint-disable-next-line prefer-destructuring
      value = value.value;
    }
  }
  if (value !== undefined) {
    if (paramDescription[encodingFormatUri] !== undefined && value.encodingFormat !== undefined) {
      if (paramDescription[encodingFormatUri][0]['@value'] !== value.encodingFormat) {
        throw new NotFoundError(
          'The encoding format of the parameter is not correct, please check the semantic description!'
        );
      }
    }
    if (value.encodingFormat !== undefined) {
      // eslint-disable-next-line prefer-destructuring
      value = value.value;
    }
  }
  if (value !== undefined) {
    if (paramDescription[minValueUri] !== undefined) {
      const minValue = paramDescription[minValueUri][0]['@value'];
      if (value < minValue) {
        throw new NotFoundError(
          'The value that you provided is smaller then the minimum value, please check the semantic description!'
        );
      }
    }
  }

  if (value !== undefined) {
    if (paramDescription[maxValueUri] !== undefined) {
      const maxValue = paramDescription[maxValueUri][0]['@value'];
      if (value > maxValue) {
        throw new NotFoundError(
          'The value that you provided is bigger then the maximum value, please check the semantic description!'
        );
      }
    }
  }

  const returnItem = {};
  const implementationProperty = mapping[implementationUri].find((e) => e['@value'])['@value'];
  returnItem[implementationProperty] = value;
  return [returnItem];
}

/**
  A Function that parses the arguments in order them to bring expected arguments into
  their native function call
  @param {object} args
  @param {array} expanded jsonld list of semantic description
  @returns {object} cleanedMappedArgObject with the native function call arguments and their values
*/
function parseArguments(args, expanded) {
  const expectedParametersNode = expanded.find((item) => item[expectedArgumentUri] !== undefined);
  validateExpectedParametersNode(expectedParametersNode, expectedArgumentUri);
  const expectedParameters = _.flatten(
    expectedParametersNode[expectedArgumentUri].map((item) => item['@list'])
  );
  if (expectedParameters.length === 0) {
    return {};
  }
  // parameter descriptions in the semantic description
  const parameterDescriptions = _.flatten(
    expectedParameters.map((parameter) =>
      expanded.filter((expandedItem) => expandedItem['@id'] === parameter['@id'])
    )
  );
  validateParameterDesc(expectedParameters, parameterDescriptions, predicateUri, parameterUri);
  validateParameterMapping(expanded, paramMappingUri, functionParamMappingUri, implementationUri);
  // parameter mapping used for native functions in the semantic description
  const paramMapping = expanded
    .filter((expandedItem) => expandedItem[paramMappingUri] !== undefined)
    .find((i) => i[paramMappingUri])[paramMappingUri];
  let mappedArgs = [];
  for (const expectedArg of expectedParameters) {
    // try to translate given args to parameter as described in parameterMapping
    mappedArgs = mappedArgs.concat(
      parseParamRecursively(expectedArg, paramMapping, parameterDescriptions, args)
    );
  }

  const mappedArgObject = mappedArgs.reduce(mergeRecursively, {});
  const cleanedMappedArgObject = _.pickBy(mappedArgObject, (v) => v !== undefined);
  return cleanedMappedArgObject;
}

/**
  A recursive helper Function that gets iterates over the expanded JSONLD
  process and capability lists and returns all the parameters
  @param {list} expandedJSONLD
  @param {boolean} requiredOnly if true, returns only the required parameters, otherwise returns all
  @returns {params} required parameters in a non-compacted and non-flattened form
*/
function getParamsRecursively(item, requiredOnly) {
  let childParams = [];
  if (item[hasPartUri]) {
    _.head(item[hasPartUri])['@list'].forEach((listItem) => {
      childParams = childParams.concat(getParamsRecursively(listItem, requiredOnly));
    });
  }
  if (item[requiredUri] && (_.head(item[requiredUri])['@value'] === true || !requiredOnly)) {
    let predicate;
    try {
      predicate = item[predicateUri][0]['@type'][0];
    } catch (e) {
      predicate = getParameters.getPredicateFromType(item['@type'], parameterUri)[0];
    }
    return [[predicate], childParams];
  }
  return null;
}

/**
  A Function that gets all the parameters of the given expanded list
  their native function call
  @param {list} expandedJSONLD
  @param {boolean} requiredOnly if true, returns only the required parameters, otherwise returns all
  @returns {params} all parameters
*/
function getParams(expandedJSONLD, requiredOnly) {
  let params = [];
  expandedJSONLD.forEach((item) => {
    params = _.compact(_.flatten(params.concat(getParamsRecursively(item, requiredOnly))));
  });
  return params;
}

/**
  A recursive helper Function that gets iterates over output description
  @param {list} outputDescription
  @returns {params} output parameters
*/

function getOutputParams(outputDescription) {
  let params = [];
  if (outputDescription[hasPartUri]) {
    _.head(outputDescription[hasPartUri])['@list'].forEach((item) => {
      params = params.concat(getOutputParams(item));
    });
  }
  const outputPredicate = getParameters.getPredicateForParam(
    outputDescription,
    outputUri,
    predicateUri
  );
  params.push(outputPredicate);
  return params;
}

/**
  A Function that checks if the output object of the capability list
  of the engine includes the parameters that are needed for the process
  @param {list} expandedProcessDesc
  @param {list} expandedCapObject
  @returns {boolean}
*/
function checkOutput(expandedProcessDesc, expandedCapObject) {
  const outputProcess = expandedProcessDesc.filter(
    (i) => i['@type'] && _.head(i['@type']) === outputUri
  );
  const outputMachine = expandedCapObject.filter(
    (i) => i['@type'] && _.head(i['@type']) === outputUri
  );
  if (outputProcess.length === 0 && outputMachine.length === 0) {
    return true;
  }
  const outputProcesParams = _.flatten(getOutputParams(_.head(outputProcess)));
  const outputMachineParams = _.flatten(getOutputParams(_.head(outputMachine)));
  return _.difference(outputProcesParams, outputMachineParams).length === 0;
}

/**
  A Function that parses the output of the native function in order to bring
  the output object into its absract format
  @param {array} expanded
  @param {object} outputObject
  @returns {object} object with its absract format and values
*/
function parseOutput(expanded, outputObject) {
  if (outputObject === undefined) {
    return null;
  }
  const deepKeys = objectDeepKeys(outputObject).map((key) => key.replace(/\./, '/'));
  const mappingNode = expanded.find((item) => item['@type'] && item['@type'].includes(mappingUri));
  if (mappingNode[returnMappingUri] === undefined) {
    return null;
  }
  validateOutputMapping(mappingNode, returnMappingUri, implementationUri, functionParamMappingUri);
  const root = _.flatten(mappingNode[returnMappingUri].map((i) => i[implementationUri])).find(
    (value) => !value['@value'].includes('/')
  )['@value'];
  const prependedKeys = deepKeys.map((key) => `${root}/${key}`);
  const parameters = mappingNode[returnMappingUri];
  const functionParameters = prependedKeys
    .map((param) =>
      parameters.find((returnItem) =>
        returnItem[implementationUri].find((item) => item['@value'] === param)
      )
    )
    .map((defaultReturnMapping) => {
      const functionParameter = defaultReturnMapping[functionParamMappingUri].pop()['@value'];
      const paramCouple = {};
      paramCouple[
        defaultReturnMapping[implementationUri].find((item) => item['@value'])['@value']
      ] = functionParameter;
      return paramCouple;
    });
  const outputNode = expanded.find(
    (expandedItem) => expandedItem['@type'] && expandedItem['@type'].includes(outputUri)
  );
  const params = _.flatten(getOutputParams(outputNode));
  if (params.includes(undefined)) {
    throw new NotFoundError(
      `PLEASE CHECK YOUR OUTPUT NODE DESCRIPTION, type of parameter cannot be parsed!
      The following output node does not have the right format: ${JSON.stringify(outputNode)}`
    );
  }
  const mappedReturnParameters = functionParameters.map((functionParameter) => {
    const mappedParameter = {};
    const idToSearch = Object.values(functionParameter)[0];
    const result = findObjectByKeyVal(outputNode, '@id', idToSearch);
    let predicate = '';
    try {
      predicate = result[predicateUri][0]['@type'][0].split('/').pop();
    } catch (e) {
      predicate = getParameters
        .getPredicateFromType(result['@type'], outputUri)[0]
        .split('/')
        .pop();
    }
    mappedParameter[Object.keys(functionParameter)[0].split('/').pop()] = predicate;
    return mappedParameter;
  });
  return replaceKeysOfReturnObject(outputObject, mappedReturnParameters);
}

module.exports = {
  findMapping,
  parseParamRecursively,
  parseOutput,
  parseArguments,
  getParams,
  checkOutput,
};