dmn-eval-js-es5
Version:
Evaluation of DMN 1.1 decision tables, limited to S-FEEL (Simple Friendly Enough Expression Language), es5 browser compatible
406 lines (379 loc) • 15.7 kB
JavaScript
;
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };
/*
* ©2017-2018 HBT Hamburger Berater Team GmbH
* All Rights Reserved.
*/
var DmnModdle = require('dmn-moddle');
var logger = require('loglevel').getLogger('dmn-eval-js');
var feel = require('../../dist/feel');
var moment = require('moment');
function createModdle(additionalPackages, options) {
return new DmnModdle(additionalPackages, options);
}
function readDmnXml(xml, opts, callback) {
return createModdle().fromXML(xml, 'dmn:Definitions', opts, callback);
}
function parseRule(rule, idx) {
var parsedRule = { number: idx + 1, input: [], inputValues: [], output: [], outputValues: [] };
rule.inputEntry.forEach(function (inputEntry) {
var text = inputEntry.text;
if (text === '') {
text = '-';
}
try {
parsedRule.input.push(feel.parse(text, {
startRule: 'SimpleUnaryTests'
}));
parsedRule.inputValues.push(text);
} catch (err) {
throw new Error('Failed to parse input entry: ' + text + ' ' + err);
}
});
rule.outputEntry.forEach(function (outputEntry) {
if (!outputEntry.text) {
parsedRule.output.push(null);
parsedRule.outputValues.push(null);
} else {
try {
parsedRule.output.push(feel.parse(outputEntry.text, {
startRule: 'SimpleExpressions'
}));
} catch (err) {
throw new Error('Failed to parse output entry: ' + outputEntry.text + ' ' + err);
}
parsedRule.outputValues.push(outputEntry.text);
}
});
return parsedRule;
}
function parseDecisionTable(decisionId, decisionTable) {
if (decisionTable.hitPolicy !== 'FIRST' && decisionTable.hitPolicy !== 'UNIQUE' && decisionTable.hitPolicy !== 'COLLECT' && decisionTable.hitPolicy !== 'RULE ORDER') {
throw new Error('Unsupported hit policy ' + decisionTable.hitPolicy);
}
var parsedDecisionTable = { hitPolicy: decisionTable.hitPolicy, rules: [], inputExpressions: [], parsedInputExpressions: [], outputNames: [] };
// parse rules (there may be none, though)
if (decisionTable.rule === undefined) {
logger.warn('The decision table for decision \'' + decisionId + '\' contains no rules.');
} else {
decisionTable.rule.forEach(function (rule, idx) {
parsedDecisionTable.rules.push(parseRule(rule, idx));
});
}
// parse input expressions
decisionTable.input.forEach(function (input) {
var inputExpression = void 0;
if (input.inputExpression && input.inputExpression.text) {
inputExpression = input.inputExpression.text;
} else if (input.inputVariable) {
inputExpression = input.inputVariable;
} else {
throw new Error('No input variable or expression set for input \'' + input.id + '\'');
}
parsedDecisionTable.inputExpressions.push(inputExpression);
try {
parsedDecisionTable.parsedInputExpressions.push(feel.parse(inputExpression, {
startRule: 'SimpleExpressions'
}));
} catch (err) {
throw new Error('Failed to parse input expression \'' + inputExpression + '\': ' + err);
}
});
// parse output names
decisionTable.output.forEach(function (output) {
if (output.name) {
parsedDecisionTable.outputNames.push(output.name);
} else {
throw new Error('No name set for output "' + output.id + '"');
}
});
return parsedDecisionTable;
}
function parseDecisions(drgElements) {
var parsedDecisions = [];
// iterate over all decisions in the DMN
drgElements.forEach(function (drgElement) {
// parse the decision table...
var decision = { decisionTable: parseDecisionTable(drgElement.id, drgElement.decisionTable), requiredDecisions: [] };
// ...and collect the decisions on which the current decision depends
if (drgElement.informationRequirement !== undefined) {
drgElement.informationRequirement.forEach(function (req) {
if (req.requiredDecision !== undefined) {
var requiredDecisionId = req.requiredDecision.href.replace('#', '');
decision.requiredDecisions.push(requiredDecisionId);
}
});
}
parsedDecisions[drgElement.id] = decision;
});
return parsedDecisions;
}
function parseDmnXml(xml, opts) {
return new Promise(function (resolve, reject) {
readDmnXml(xml, opts, function (err, dmnContent) {
if (err) {
reject(err);
} else {
try {
var decisions = parseDecisions(dmnContent.drgElements);
resolve(decisions);
} catch (err) {
reject(err);
}
}
});
});
}
function resolveExpression(expression, obj) {
var parts = expression.split('.');
return parts.reduce(function (resolved, part) {
return resolved === undefined ? undefined : resolved[part];
}, obj);
}
// Sets the given value to a nested property of the given object. The nested property is resolved from the given expression.
// If the given nested property does not exist, it is added. If it exists, it is set (overwritten). If it exists and is
// an array, the given value is added.
// Examples:
// setOrAddValue('foo.bar', { }, 10) returns { foo: { bar: 10 } }
// setOrAddValue('foo.bar', { foo: { }, 10) returns { foo: { bar: 10 } }
// setOrAddValue('foo.bar', { foo: { bar: 9 }, 10) returns { foo: { bar: 10 } }
// setOrAddValue('foo.bar', { foo: { bar: [ ] }, 10) returns { foo: { bar: [ 10 ] } }
// setOrAddValue('foo.bar', { foo: { bar: [ 9 ] }, 10) returns { foo: { bar: [9, 10 ] } }
function setOrAddValue(expression, obj, value) {
var indexOfDot = expression.indexOf('.');
if (indexOfDot < 0) {
if (obj[expression] && Array.isArray(obj[expression])) {
obj[expression].push(value); // eslint-disable-line no-param-reassign
} else {
obj[expression] = value; // eslint-disable-line no-param-reassign
}
} else {
var first = expression.substr(0, indexOfDot);
var remainder = expression.substr(indexOfDot + 1);
if (obj[first]) {
setOrAddValue(remainder, obj[first], value);
} else {
obj[first] = setOrAddValue(remainder, {}, value); // eslint-disable-line no-param-reassign
}
}
return obj;
}
// merge the result of the required decision into the context so that it is available as input for the requested decision
function mergeContext(context, additionalContent) {
var aggregate = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
if (Array.isArray(additionalContent)) {
// additional content is the result of evaluation a rule table with multiple rule results
additionalContent.forEach(function (ruleResult) {
return mergeContext(context, ruleResult, true);
});
} else {
// additional content is the result of evaluation a rule table with a single rule result
for (var prop in additionalContent) {
// eslint-disable-line no-restricted-syntax
if (additionalContent.hasOwnProperty(prop)) {
var value = additionalContent[prop];
if (Array.isArray(context[prop])) {
if (Array.isArray(value)) {
context[prop] = context[prop].concat(value); // eslint-disable-line no-param-reassign
} else if (value !== null && value !== undefined) {
context[prop].push(value); // eslint-disable-line no-param-reassign
}
} else if ((typeof value === 'undefined' ? 'undefined' : _typeof(value)) === 'object' && value !== null && !moment.isMoment(value) && !moment.isDate(value) && !moment.isDuration(value)) {
if (context[prop] === undefined || context[prop] === null) {
context[prop] = {}; // eslint-disable-line no-param-reassign
}
mergeContext(context[prop], value, aggregate);
} else if (aggregate) {
context[prop] = []; // eslint-disable-line no-param-reassign
context[prop].push(value); // eslint-disable-line no-param-reassign
} else {
context[prop] = value; // eslint-disable-line no-param-reassign
}
}
}
}
}
function evaluateRule(rule, resolvedInputExpressions, outputNames, context) {
for (var i = 0; i < rule.input.length; i += 1) {
try {
var inputFunction = rule.input[i].build(context); // eslint-disable-line no-await-in-loop
var input = resolvedInputExpressions[i];
if (!inputFunction(input)) {
return {
matched: false
};
}
} catch (err) {
logger.error(err);
throw new Error('Failed to evaluate input condition in column ' + (i + 1) + ': \'' + rule.inputValues[i] + '\': ' + err);
}
}
var outputObject = {};
for (var _i = 0; _i < rule.output.length; _i += 1) {
if (rule.output[_i] !== null) {
var outputValue = rule.output[_i].build(context); // eslint-disable-line no-await-in-loop
setOrAddValue(outputNames[_i], outputObject, outputValue[0]);
} else {
setOrAddValue(outputNames[_i], outputObject, undefined);
}
}
return { matched: true, output: outputObject };
}
function evaluateDecision(decisionId, decisions, context, alreadyEvaluatedDecisions) {
if (!alreadyEvaluatedDecisions) {
alreadyEvaluatedDecisions = []; // eslint-disable-line no-param-reassign
}
var decision = decisions[decisionId];
if (decision === undefined) {
throw new Error('No such decision "' + decisionId + '"');
}
// execute required decisions recursively first
for (var i = 0; i < decision.requiredDecisions.length; i += 1) {
var reqDecision = decision.requiredDecisions[i];
// check if the decision was already executed, to prevent unecessary evaluations if multiple decisions require the same decision
if (!alreadyEvaluatedDecisions[reqDecision]) {
logger.debug('Need to evaluate required decision ' + reqDecision);
var requiredResult = evaluateDecision(reqDecision, decisions, context, alreadyEvaluatedDecisions); // eslint-disable-line no-await-in-loop
mergeContext(context, requiredResult);
alreadyEvaluatedDecisions[reqDecision] = true; // eslint-disable-line no-param-reassign
}
}
logger.info('Evaluating decision "' + decisionId + '"...');
logger.debug('Context: ' + JSON.stringify(context));
var decisionTable = decision.decisionTable;
// resolve input expressions
var resolvedInputExpressions = [];
for (var _i2 = 0; _i2 < decisionTable.parsedInputExpressions.length; _i2 += 1) {
var parsedInputExpression = decisionTable.parsedInputExpressions[_i2];
var plainInputExpression = decisionTable.inputExpressions[_i2];
try {
var resolvedInputExpression = parsedInputExpression.build(context); // eslint-disable-line no-await-in-loop
resolvedInputExpressions.push(resolvedInputExpression[0]);
} catch (err) {
throw new Error('Failed to evaluate input expression ' + plainInputExpression + ' of decision ' + decisionId + ': ' + err);
}
}
// initialize the result to an object with undefined output values (hit policy FIRST or UNIQUE) or to an empty array (hit policy COLLECT or RULE ORDER)
var decisionResult = decisionTable.hitPolicy === 'FIRST' || decisionTable.hitPolicy === 'UNIQUE' ? {} : [];
decisionTable.outputNames.forEach(function (outputName) {
if (decisionTable.hitPolicy === 'FIRST' || decisionTable.hitPolicy === 'UNIQUE') {
setOrAddValue(outputName, decisionResult, undefined);
}
});
// iterate over the rules of the decision table of the requested decision,
// and either return the output of the first matching rule (hit policy FIRST)
// or collect the output of all matching rules (hit policy COLLECT)
var hasMatch = false;
var _loop = function _loop(_i3) {
var rule = decisionTable.rules[_i3];
var ruleResult = void 0;
try {
ruleResult = evaluateRule(rule, resolvedInputExpressions, decisionTable.outputNames, context); // eslint-disable-line no-await-in-loop
} catch (err) {
throw new Error('Failed to evaluate rule ' + rule.number + ' of decision ' + decisionId + ': ' + err);
}
if (ruleResult.matched) {
// only one match for hit policy UNIQUE!
if (hasMatch && decisionTable.hitPolicy === 'UNIQUE') {
throw new Error('Decision "' + decisionId + '" is not unique but hit policy is UNIQUE.');
}
hasMatch = true;
logger.info('Result for decision "' + decisionId + '": ' + JSON.stringify(ruleResult.output) + ' (rule ' + (_i3 + 1) + ' matched)');
// merge the result of the matched rule
if (decisionTable.hitPolicy === 'FIRST' || decisionTable.hitPolicy === 'UNIQUE') {
decisionTable.outputNames.forEach(function (outputName) {
var resolvedOutput = resolveExpression(outputName, ruleResult.output);
if (resolvedOutput !== undefined || decisionTable.hitPolicy === 'FIRST' || decisionTable.hitPolicy === 'UNIQUE') {
setOrAddValue(outputName, decisionResult, resolvedOutput);
}
});
if (decisionTable.hitPolicy === 'FIRST') {
// no more rule results in this case
return 'break';
}
} else {
decisionResult.push(ruleResult.output);
}
}
};
for (var _i3 = 0; _i3 < decisionTable.rules.length; _i3 += 1) {
var _ret = _loop(_i3);
if (_ret === 'break') break;
}
if (!hasMatch && decisionTable.rules.length > 0) {
logger.warn('No rule matched for decision "' + decisionId + '".');
}
return decisionResult;
}
function dumpTree(node, indent) {
if (!node) {
logger.debug('undefined');
return;
}
if (!indent) {
indent = ''; // eslint-disable-line no-param-reassign
}
logger.debug(indent + node.type);
if (node.not) {
logger.debug(indent + ' (not)');
}
if (node.operator) {
logger.debug(indent + ' ' + node.operator);
}
var newIndent = indent + ' ';
switch (node.type) {
case 'ArithmeticExpression':
{
dumpTree(node.operand_1, newIndent);
dumpTree(node.operand_2, newIndent);
break;
}
case 'FunctionInvocation':
{
dumpTree(node.fnName, newIndent);
dumpTree(node.params, newIndent);
break;
}
case 'DateTimeLiteral':
node.params.forEach(function (p) {
return dumpTree(p, newIndent);
});break;
case 'Interval':
{
dumpTree(node.startpoint, newIndent);
dumpTree(node.endpoint, newIndent);
break;
}
case 'Literal':
logger.debug(newIndent + '"' + node.value + '"');break;
case 'Name':
logger.debug(newIndent + '"' + node.nameChars + '"');break;
case 'Program':
dumpTree(node.body, newIndent);break;
case 'PositionalParameters':
node.params.forEach(function (p) {
return dumpTree(p, newIndent);
});break;
case 'QualifiedName':
node.names.forEach(function (n) {
return dumpTree(n, newIndent);
});break;
case 'SimpleExpressions':
node.simpleExpressions.forEach(function (s) {
return dumpTree(s, newIndent);
});break;
case 'SimplePositiveUnaryTest':
dumpTree(node.operand, newIndent);break;
case 'SimpleUnaryTestsNode':
node.expr.forEach(function (e) {
return dumpTree(e, newIndent);
});break;
case 'UnaryTestsNode':
node.expr.forEach(function (e) {
return dumpTree(e, newIndent);
});break;
default:
logger.debug('?');
}
}
module.exports = { readDmnXml: readDmnXml, parseDmnXml: parseDmnXml, parseDecisions: parseDecisions, evaluateDecision: evaluateDecision, dumpTree: dumpTree };