@hbtgmbh/dmn-eval-js
Version:
Evaluation of DMN 1.1 decision tables, limited to S-FEEL (Simple Friendly Enough Expression Language)
276 lines (248 loc) • 10.3 kB
JavaScript
/*
*
* ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary),
* Bangalore, India. All Rights Reserved.
*
*/
const _ = require('lodash');
const fnGen = require('../utils/helper/fn-generator');
const addKwargs = require('../utils/helper/add-kwargs');
const builtInFns = require('../utils/built-in-functions');
const resolveName = require('../utils/helper/name-resolution.js');
const logger = require('loglevel').getLogger('dmn-eval-js');
module.exports = function (ast) {
ast.ProgramNode.prototype.build = function (data = {}, env = {}, type = 'output') {
let args = {};
if (!data.isContextBuilt) {
const context = Object.assign({}, data, builtInFns);
args = Object.assign({}, { context }, env);
args.isContextBuilt = true;
} else {
args = data;
}
// bodybuilding starts here...
// let's pump some code ;)
const result = this.body.build(args);
if (type === 'input') {
if (typeof result === 'function') {
return result;
}
const fnResult = function (x) {
return x === result;
};
return fnResult;
}
return result;
};
ast.IntervalStartLiteralNode.prototype.build = function () {
return fnGen(this.intervalType);
};
ast.IntervalEndLiteralNode.prototype.build = function () {
return fnGen(this.intervalType);
};
ast.IntervalNode.prototype.build = function (args) {
const startpoint = this.startpoint.build(args);
const endpoint = this.endpoint.build(args);
return (x) => {
const startValue = this.intervalstart.build()(startpoint)(x);
const endValue = this.intervalend.build()(endpoint)(x);
return startValue === undefined || endValue === undefined ? undefined : startValue && endValue;
};
};
ast.SimplePositiveUnaryTestNode.prototype.build = function (args) {
const result = this.operand.build(args);
// hack to treat input expressions as input variables and let functions in input entries reference them
// for example: starts with(name, prefix)
// where "name" is the input expression
// for this to work, if the result of the function is true (like in the example above), that value cannot be
// compared with the the evaluated input expression (which is the value of the input variable), so we must
// patch the comparison here
if (args.context._inputVariableName && this.operand.type === 'FunctionInvocation' && this.operand.params) {
// patch only if there is an input variable and the simple positive unary test contains a function directly,
// where the input variable in a parameter of that function
const nodeIsQualifiedNameOfInputVariable = node =>
node.type === 'QualifiedName' && node.names.map(nameNode => nameNode.nameChars).join('.') === args.context._inputVariableName;
const inputVariableParameter = (this.operand.params.params || []).find(node => nodeIsQualifiedNameOfInputVariable(node));
if (inputVariableParameter) {
if (result === true) {
// if the function evaluates to true, compare the evaluated input expression with the evaluated input variable,
// not with the result of the function evaluation
return fnGen(this.operator || '==')(_, inputVariableParameter.build(args));
} else if (result === false) {
// if the function evaluates to false, the simple positive unary test should always evaluate to false
return () => false;
}
}
}
return fnGen(this.operator || '==')(_, result);
};
ast.SimpleUnaryTestsNode.prototype.build = function (data = {}) {
const context = Object.assign({}, data, builtInFns);
const args = { context };
if (this.expr) {
const results = this.expr.map(d => d.build(args));
if (this.not) {
const negResults = results.map(result => args.context.not(result));
return x => negResults.reduce((result, next) => {
const nextValue = next(x);
return (result === false || nextValue === false) ? false : ((result === undefined || nextValue === undefined) ? undefined : (result && nextValue));
}, true);
}
return x => results.reduce((result, next) => {
const nextValue = next(x);
return (result === true || nextValue === true) ? true : ((result === undefined || nextValue === undefined) ? undefined : (result || nextValue));
}, false);
}
return () => true;
};
ast.QualifiedNameNode.prototype.build = function (args, doNotWarnIfUndefined = false) {
const [first, ...remaining] = this.names;
const buildNameNode = (name) => {
const result = { nameNode: name, value: name.build(null, false) };
return result;
};
const processRemaining = (firstResult, firstExpression) => remaining.map(buildNameNode)
.reduce((prev, next) => {
if (prev.value === undefined) {
return prev;
}
return { value: prev.value[next.value], expression: `${prev.expression}.${next.nameNode.nameChars}` };
}, { value: firstResult, expression: firstExpression });
const firstResult = first.build(args);
if (remaining.length) {
const fullResult = processRemaining(firstResult, first.nameChars);
if (fullResult.value === undefined) {
if (!doNotWarnIfUndefined) {
logger.info(`'${fullResult.expression}' resolved to undefined`);
}
}
return fullResult.value;
}
if (firstResult === undefined) {
if (!doNotWarnIfUndefined) {
logger.info(`'${first.nameChars}' resolved to undefined`);
}
}
return firstResult;
};
ast.ArithmeticExpressionNode.prototype.build = function (args) {
const operandsResult = [this.operand_1, this.operand_2].map((op) => {
if (op === null) {
return 0;
}
return op.build(args);
});
return fnGen(this.operator)(operandsResult[0])(operandsResult[1]);
};
ast.SimpleExpressionsNode.prototype.build = function (data = {}, env = {}) {
let context = {};
if (!data.isBuiltInFn) {
context = Object.assign({}, data, builtInFns, { isBuiltInFn: true });
} else {
context = data;
}
const args = Object.assign({}, { context }, env);
return this.simpleExpressions.map(d => d.build(args));
};
// _fetch is used to return the name string or
// the value extracted from context or kwargs using the name string
ast.NameNode.prototype.build = function (args, _fetch = true) {
const name = this.nameChars;
if (!_fetch) {
return name;
}
return resolveName(name, args);
};
ast.LiteralNode.prototype.build = function () {
return this.value;
};
ast.DateTimeLiteralNode.prototype.build = function (args) {
const fn = args.context[this.symbol];
const paramsResult = this.params.map(d => d.build(args));
let result;
if (!paramsResult.includes(undefined)) {
result = fn(...paramsResult);
}
return result;
};
// Invoking function defined as boxed expression in the context entry
// See ast.FunctionDefinitionNode for details on declaring function
// Function supports positional as well as named parameters
ast.FunctionInvocationNode.prototype.build = function (args) {
const processFormalParameters = (formalParams) => {
const values = this.params.build(args);
if (formalParams && values && Array.isArray(values)) {
const kwParams = values.reduce((recur, next, i) => {
const obj = {};
obj[formalParams[i]] = next;
return Object.assign({}, recur, obj);
}, {});
return addKwargs(args, kwParams);
}
return addKwargs(args, values);
};
const processUserDefinedFunction = (fnMeta) => {
const fn = fnMeta.fn;
const formalParams = fnMeta.params;
if (formalParams) {
return fn.build(processFormalParameters(formalParams));
}
return fn.build(args);
};
const processInBuiltFunction = (fnMeta) => {
const doNotWarnIfUndefined = fnMeta.name === 'defined';
const values = this.params.build(args, doNotWarnIfUndefined);
if (Array.isArray(values)) {
return fnMeta(...[...values, args.context]);
}
return fnMeta(Object.assign({}, args.context, args.kwargs), values);
};
const processDecision = (fnMeta) => {
const expr = fnMeta.expr;
if (expr.body instanceof ast.FunctionDefinitionNode) {
const exprResult = expr.body.build(args);
return processUserDefinedFunction(exprResult);
}
const formalParametersResult = processFormalParameters();
return expr.build(formalParametersResult);
};
const processFnMeta = (fnMeta) => {
if (typeof fnMeta === 'function') {
return processInBuiltFunction(fnMeta);
} else if (typeof fnMeta === 'object' && fnMeta.isDecision) {
return processDecision(fnMeta);
}
return processUserDefinedFunction(fnMeta);
};
const fnNameResult = this.fnName.build(args);
let result;
if (fnNameResult !== undefined) {
result = processFnMeta(fnNameResult);
}
return result;
};
ast.PositionalParametersNode.prototype.build = function (args, doNotWarnIfUndefined = false) {
const results = this.params.map(d => d.build(args, doNotWarnIfUndefined));
return results;
};
ast.ComparisonExpressionNode.prototype.build = function (args) {
let operator = this.operator;
if (operator === 'between') {
const results = [this.expr_1, this.expr_2, this.expr_3].map(d => d.build(args));
if ((results[0] >= results[1]) && (results[0] <= results[2])) {
return true;
}
return false;
} else if (operator === 'in') {
const processExpr = (operand) => {
this.expr_2 = Array.isArray(this.expr_2) ? this.expr_2 : [this.expr_2];
const tests = this.expr_2.map(d => d.build(args));
return tests.map(test => test(operand)).reduce((accu, next) => accu || next, false);
};
return processExpr(this.expr_1.build(args));
}
const results = [this.expr_1, this.expr_2].map(d => d.build(args));
operator = operator !== '=' ? operator : '==';
return fnGen(operator)(results[0])(results[1]);
};
};