File

libs/ngx-pfe/pfe-conditions/rules-evaluator/rules-evaluator.ts

Description

This PfeRulesEvaluator is used to evaluate the rules used in the navigation configuration.

Index

Methods

Methods

Static evaluateAdvancedConditions
evaluateAdvancedConditions(conditionAdvanced: ConditionAdvanced, state: PfeUserInputState)

Evaluate Advanced Conditions

Parameters :
Name Type Optional Description
conditionAdvanced ConditionAdvanced No

object that describe the operator and conditions

state PfeUserInputState No

The current state.

Returns : boolean

true if all conditions are true, false if at least one is false.

Static evaluateCondition
evaluateCondition(condition: ExpressionCondition, state: PfeUserInputState)

Evaluates a condition. Possible comparator are: =, !=, >, < A condition always consists of three parts. For example: a = b Conditions can also contain strings with whitespace: a = 'test with space'

Parameters :
Name Type Optional Description
condition ExpressionCondition No

The condition to evaluate.

state PfeUserInputState No

The current state.

Returns : boolean

boolean that shows, if the condition was true.

Static evaluateConditions
evaluateConditions(conditions: RulesEvaluatorConditions, state: PfeUserInputState)

Evaluates all the conditions of a navigational rule. Conditions are connected by AND.

Parameters :
Name Type Optional Description
conditions RulesEvaluatorConditions No

The conditions to check.

state PfeUserInputState No

The current state.

Returns : boolean

true if all conditions are true, false if at least one is false.

Static evaluateORConditions
evaluateORConditions(conditions: ExpressionCondition[], state: PfeUserInputState)

Evaluates all the conditions of a navigational rule. Conditions are connected by OR.

Parameters :
Name Type Optional Description
conditions ExpressionCondition[] No

The conditions to check.

state PfeUserInputState No

The current state.

Returns : boolean

true if all conditions are true, false if at least one is false.

Static getReplacer
getReplacer(state: PfeUserInputState)

Returns a replacer function that replaces jsonpath expressions with their corresponding values.

Parameters :
Name Type Optional Description
state PfeUserInputState No

The current state.

Returns : (match: string) => any

The replacer function.

Static getStatePropertyName
getStatePropertyName(conditions: ExpressionCondition[])

Retrieve the state property names of the first part of the conditions. propertyName in case of $state.propertyName or value unchanged

Parameters :
Name Type Optional Description
conditions ExpressionCondition[] No

The conditions to parse.

Returns : string[]

The state property names of the condition's first part.

import { parseScript } from 'esprima-next';
import ESTree from 'estree';
import evaluate from 'static-eval';
import { ExpressionCondition, ConditionAdvanced, OPERATORS, RulesEvaluatorConditions } from './condition.model';
import { PfeUtilService } from '../../pfe-util/services/util.service';
import { PfeUserInputState } from '../../models/pfe-state/user-input-state.model';

import { isObject, isString } from '../../util/type-checks';

// used for iterating over all capture groups with content {.*}
export const JSON_PATH_REGEX = /({.*?})/gm;
/**
 * This PfeRulesEvaluator is used to evaluate the rules used in the navigation
 * configuration.
 *
 * @export
 */
export class PfeRulesEvaluator {
  /**
   * Evaluates all the conditions of a navigational rule.
   * Conditions are connected by AND.
   *
   * @param conditions The conditions to check.
   * @param state The current state.
   * @returns true if all conditions are true, false if at least one is false.
   */
  static evaluateConditions(conditions: RulesEvaluatorConditions, state: PfeUserInputState): boolean {
    if (Array.isArray(conditions)) {
      let result = false;
      if (conditions) {
        for (const condition of conditions) {
          result = PfeRulesEvaluator.evaluateCondition(condition, state);
          if (result === false) {
            return result;
          }
        }
      }
      return result;
    }
    return PfeRulesEvaluator.evaluateAdvancedConditions(conditions, state);
  }

  /**
   * Evaluates all the conditions of a navigational rule.
   * Conditions are connected by OR.
   *
   * @param conditions The conditions to check.
   * @param state The current state.
   * @returns true if all conditions are true, false if at least one is false.
   */
  static evaluateORConditions(conditions: ExpressionCondition[], state: PfeUserInputState): boolean {
    let result = false;
    if (conditions) {
      for (const condition of conditions) {
        result = PfeRulesEvaluator.evaluateCondition(condition, state);
        if (result === true) {
          return result;
        }
      }
    }
    return result;
  }

  /**
   * Evaluate Advanced Conditions
   *
   * @param conditionAdvanced object that describe the operator and conditions
   * @param state The current state.
   * @returns true if all conditions are true, false if at least one is false.
   */
  static evaluateAdvancedConditions(conditionAdvanced: ConditionAdvanced, state: PfeUserInputState): boolean {
    return conditionAdvanced.operator === OPERATORS.AND
      ? PfeRulesEvaluator.evaluateConditions(conditionAdvanced.conditions, state)
      : PfeRulesEvaluator.evaluateORConditions(conditionAdvanced.conditions, state);
  }

  /**
   * Evaluates a condition.
   * Possible comparator are:
   * =, !=, >, <
   * A condition always consists of three parts. For example:
   * a = b
   * Conditions can also contain strings with whitespace:
   * a = 'test with space'
   *
   * @param condition The condition to evaluate.
   * @param state The current state.
   * @returns boolean that shows, if the condition was true.
   */
  public static evaluateCondition(condition: ExpressionCondition, state: PfeUserInputState): boolean {
    try {
      let evalResult = false;

      if (condition.expression !== undefined) {
        const expression = condition.expression;

        if (isString(expression)) {
          const replacer = this.getReplacer(state);

          let statement = expression.replace(JSON_PATH_REGEX, replacer);
          // esprima can not handle undefined.
          // So undefined == undefined evaluates to false and even !undefined evaluates to false
          // To prevent this we convert any undefined to null.
          statement = statement.replace(/undefined/gm, 'null');

          const ast = (parseScript(statement).body[0] as ESTree.ExpressionStatement).expression;
          evalResult = !!evaluate(ast, {}) as boolean;
        } else {
          evalResult = !!expression;
        }
      }

      if (condition.value1Expression !== undefined) {
        const value1 = condition.value1Expression;
        let expression1Result = value1;
        // If the first expression is not a string, there it not have get the value
        if (isString(value1)) {
          const jpe1 = value1 as string;
          expression1Result = PfeUtilService.getValueOrList(state, jpe1, true);
          // Historically, if there is only one expression the result is not threated as a string
          // Is threated as a real boolean
          if (!condition.operator && condition.value2Expression === undefined && expression1Result) {
            return true;
          }
        }

        if (condition.operator && condition.value2Expression !== undefined) {
          const value2 = condition.value2Expression;
          let expression2Result = value2;
          if (isString(value2)) {
            const jpe2 = value2 as string;
            expression2Result = PfeUtilService.getValueOrList(state, jpe2, true);
          }

          // If the "value1AsReal" is not true, then the value is treated as string.
          expression1Result = JSON.stringify(String(expression1Result));
          expression2Result = JSON.stringify(String(expression2Result));

          // We compare the two expressions
          const expression = `${expression1Result} ${condition.operator} ${expression2Result}`;
          const ast = (parseScript(expression).body[0] as ESTree.ExpressionStatement).expression;
          evalResult = evaluate(ast, {}) as boolean;
        }
      }

      return evalResult;
    } catch (error) {
      console.error('Error evaluating rule', condition, error);
      return false;
    }
  }

  /**
   * Returns a replacer function that replaces jsonpath expressions with
   * their corresponding values.
   *
   * @param state The current state.
   * @returns The replacer function.
   */
  public static getReplacer(state: PfeUserInputState) {
    return (match: string) => {
      // {$.aTestKey} -> $.aTestKey
      const jsonPath = match.replace(/{|}/gm, '');
      const value = PfeUtilService.getValueOrList(state, jsonPath, true);

      if (isString(value)) {
        return JSON.stringify(String(value));
      } else if (isObject(value) && !Array.isArray(value)) {
        // can't serialize and evaluate an object
        return '"' + Math.random() + '"';
      } else if (Array.isArray(value)) {
        // Return the Array as a string
        return JSON.stringify(value);
      }
      return value;
    };
  }

  /**
   * Retrieve the state property names of the first part of the conditions.
   * propertyName in case of $state.propertyName or value unchanged
   *
   * @param conditions The conditions to parse.
   * @returns The state property names of the condition's first part.
   */
  public static getStatePropertyName(conditions: ExpressionCondition[]): string[] {
    const propertyNames = [];
    const FIRST_KEY_REGEX = /\$\.(.*?)(?:\.|$)/g;

    if (conditions) {
      for (const condition of conditions) {
        const value1 = condition.value1Expression as string;
        const match = FIRST_KEY_REGEX.exec(value1);
        if (match && match.length > 0) {
          propertyNames.push(match[1]);
        }
      }
    }
    return propertyNames;
  }
}

results matching ""

    No results matching ""