Source: engine/universal/decider/hard_constraint_evaluation/hc-eval.js

const { information } = require('@proceed/machine');
const HardConstraint = require('./hardconstraint.js');

/**
 * @memberof module:@proceed/decider
 * @class
 *
 * Evaluator for HardConstraints
 * @hideconstructor
 */
class HardConstraintEvaluator {
  /**
   * Evaluation of nested hardConstraint with use of machineInformation
   * @param {hardConstraint}
   * @param {infos} machineinformation
   * @returns boolean result of evaluation
   */
  static evaluateNestedHardConstraint(nestedHardConstraint, infos) {
    const {
      name,
      condition,
      values,
      _valuesAttributes: { conjunction },
      hardConstraints,
    } = nestedHardConstraint;
    // every subconstraint of nested constraint (constraints in hardConstraints-array with added properties of parent constraint)
    const evaluatedSubConstraintList = values.map((value) => {
      const subConstraint = hardConstraints.map((hc) => {
        const hardConstraintsElement = { ...hc };
        if (hardConstraintsElement._type === 'hardConstraint') {
          hardConstraintsElement.name = `${name + condition + value.value}.${
            hardConstraintsElement.name
          }`; // concat value-attributes with name of hardConstraints-Element
        } else {
          hardConstraintsElement.constraintGroup = hardConstraintsElement.constraintGroup.map(
            (cg) => {
              const constraintGroupElement = { ...cg };
              if (cg._type === 'hardConstraint') {
                constraintGroupElement.name = `${name + condition + value.value}.${
                  constraintGroupElement.name
                }`;
              }
              return constraintGroupElement;
            }
          );
        }
        return hardConstraintsElement;
      });

      const evaluatedSubConstraint = this.evaluateAllConstraints(subConstraint, infos);
      return evaluatedSubConstraint;
    });

    if (conjunction === 'OR')
      return evaluatedSubConstraintList.some((evaluatedSubConstraint) => evaluatedSubConstraint);
    return evaluatedSubConstraintList.every((evaluatedSubConstraint) => evaluatedSubConstraint);
  }

  /**
   * Evaluation of hardConstraint with use of machineInformation
   * @param {hardConstraint}
   * @param {infos} machineinformation
   * @returns boolean result of evaluation
   */
  static evaluateHardConstraint(hardConstraint, infos) {
    const {
      name,
      condition,
      values,
      _valuesAttributes: { conjunction },
      hardConstraints,
    } = hardConstraint;

    if (hardConstraints === undefined) {
      const searchedInfo = Object.keys(infos).find((info) => info.replace('_', '.') === name);
      return new HardConstraint({
        name,
        condition,
        values,
        conjunction,
      }).satisfiedByMachine(infos[searchedInfo]); // evaluate hardconstraint using machineinfo
    } // nested constraint
    return this.evaluateNestedHardConstraint(hardConstraint, infos);
  }

  /**
   * Evaluation of all constraintGroups with use of machineInformation
   * @param [constraintGroups]
   * @param {infos} machineinformation
   * @returns boolean result of evaluation
   */
  static evaluateAllConstraintGroups(constraintGroups, infos) {
    const evaluatedConstraintGroupList = [];
    const referencedConstraintGroupList = [];

    constraintGroups.forEach((cg) => {
      const {
        _attributes: { id, conjunction },
        constraintGroup,
      } = cg;

      const evaluatedSubConstraintList = constraintGroup.map((subConstraint) => {
        if (subConstraint._type === 'constraintGroupRef') {
          // result of evaluation for referenced constraintgroup
          const referencedConstraintGroup = evaluatedConstraintGroupList.find(
            (group) => group.id === subConstraint._attributes.ref
          );
          referencedConstraintGroupList.push(referencedConstraintGroup.id);
          return referencedConstraintGroup.result;
        } // else: hardConstraint
        // result of evaluation for hardconstraint
        return this.evaluateHardConstraint(subConstraint, infos);
      });

      let result;
      // evaluation of constraintgroup based on conjunction
      if (conjunction === 'OR')
        result = evaluatedSubConstraintList.some(
          (evaluatedSubConstraint) => evaluatedSubConstraint
        );
      else {
        result = evaluatedSubConstraintList.every(
          (evaluatedSubConstraint) => evaluatedSubConstraint
        );
      }
      evaluatedConstraintGroupList.push({ id, result }); // evaluated constraintgroup
    });

    return evaluatedConstraintGroupList.every((group) => {
      if (!referencedConstraintGroupList.includes(group.id)) return group.result;
      return true;
    });
  }

  /**
   * Evaluation of all constraints with use of machineInformation,
   * returns boolean with evalution-result
   *
   * @param [constraints]
   * @param {infos} machineinformation
   * @returns boolean evaluationResult - result of evaluation
   */
  static evaluateAllConstraints(constraints, infos) {
    const constraintGroups = constraints.filter(
      (constraint) => constraint._type === 'constraintGroup'
    );
    const evaluationResultConstraintGroups = this.evaluateAllConstraintGroups(
      constraintGroups,
      infos
    );

    const hardConstraints = constraints.filter(
      (constraint) => constraint._type === 'hardConstraint'
    );
    const evaluationResultHardConstraints = hardConstraints.every((hardConstraint) =>
      this.evaluateHardConstraint(hardConstraint, infos)
    );

    const evaluationResult = evaluationResultConstraintGroups && evaluationResultHardConstraints;
    return evaluationResult;
  }

  /**
   * Evaluation of process-execution constraints with use of information of current execution,
   * returns list of unfulfilled constraints
   *
   * @param {Array} constraints - constraints
   * @param {infos} execution information
   * @returns {Array} unfulfilledConstraints
   */
  static evaluateExecutionConstraints(constraints, infos) {
    const unfulfilledConstraints = constraints.filter((constraint) => {
      const { name, values } = constraint;
      if (name === 'maxTime') {
        return values.some((value) => infos.time > value.value);
      }
      if (name === 'maxTimeGlobal') {
        return values.some((value) => infos.timeGlobal > value.value);
      }
      if (name === 'maxMachineHops') {
        return values.some((value) => infos.machineHops > value.value);
      }
      if (name === 'maxTokenStorageTime') {
        return values.some((value) => infos.storageTime > value.value);
      }
      if (name === 'maxTokenStorageRounds') {
        return values.some((value) => infos.storageRounds > value.value);
      }
      return false;
    });

    return unfulfilledConstraints;
  }

  /**
   * Iterates through array of constraints to retrieve every constraint name
   * @param [constraints]
   * @returns [hardConstraintNames]
   */
  static getHardConstraintNames(constraints) {
    const hardConstraintNames = new Set();
    constraints.forEach((constraint) => {
      if (constraint._type === 'hardConstraint') {
        const { name, condition, values, hardConstraints } = constraint;
        if (hardConstraints !== undefined) {
          // nested constraint
          const subConstraintNames = this.getHardConstraintNames(hardConstraints);
          subConstraintNames.forEach((subConstraintName) => {
            values.forEach((value) => {
              hardConstraintNames.add(
                `${name + condition + value.value.replace('.', '_')}.${subConstraintName}`
              );
            });
          });
        } else {
          hardConstraintNames.add(name);
        }
      } else if (constraint._type === 'constraintGroup') {
        // constraintGroup
        const { constraintGroup } = constraint;
        const groupNames = this.getHardConstraintNames(constraintGroup);
        groupNames.forEach((groupName) => hardConstraintNames.add(groupName));
      }
    });
    return [...hardConstraintNames];
  }

  /**
   * Returns true, if the machine satisfies all hard constraints.
   *
   * 1. Find every constraint name in constraints-array
   * 2. Get machineInformation for hard constraint names
   * 3. Evaluation of every constraint based on machineInformation
   * 4. Return true if every constraint is satisfied
   *
   * @param [constraints] Constraints to check on machine
   * @returns boolean If all hardconstraints are satisfied
   */

  static async machineSatisfiesAllHardConstraints(constraints) {
    const hardConstraintNames = this.getHardConstraintNames(constraints);

    const informationCategories = hardConstraintNames.reduce((categories, hardConstraintName) => {
      const [newCategory] = hardConstraintName.replace('machine.', '').split('.');
      if (!categories.includes(newCategory)) {
        categories.push(newCategory);
      }

      return categories;
    }, []);

    const informationCategoriesResult = await information.getMachineInformation(
      informationCategories
    );

    const infos = hardConstraintNames.reduce((values, hardConstraintName) => {
      let name = hardConstraintName.replace('machine.', '');

      let deepValue;

      while (name) {
        const [subscript, nextName] = name.split('.');

        if (!deepValue) {
          deepValue = informationCategoriesResult[subscript];
        } else if (Array.isArray(deepValue)) {
          deepValue = deepValue.map((value) => value[subscript]);
        } else {
          deepValue = deepValue[subscript];
        }

        name = nextName;
      }

      return { ...values, [hardConstraintName]: deepValue };
    }, {});

    return this.evaluateAllConstraints(constraints, infos);
  }
}
module.exports = HardConstraintEvaluator;