UNPKG

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
'use strict'; 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 };