js-feel
Version:
FEEL(Friendly Enough Expression Language) based on DMN specification 1.1 for conformance level 3
233 lines (208 loc) • 7.86 kB
JavaScript
/*
*
* ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary),
* Bangalore, India. All Rights Reserved.
*
*/
const _ = require('lodash');
const FEEL = require('../../dist/feel').parse;
const { getOrderedOutput, hitPolicyPass } = require('./hit-policy.js');
function Node(data, type) {
this.data = data;
this.type = type;
this.children = {};
}
function Tree(data) {
const node = new Node(data, 'Root');
this.root = node;
}
function generatePriorityList(dTable) {
const outputs = dTable.outputs && Array.isArray(dTable.outputs) ? dTable.outputs : [dTable.outputs];
const outputValuesList = dTable.outputValues;
// .map(outputValue => FEEL(outputValue));
const ruleList = dTable.ruleList;
const numOfConditions = dTable.inputExpressionList.length;
const calcPriority = (priorityMat, outputs) => {
const sortedPriority = _.sortBy(priorityMat, outputs);
const rulePriority = {};
sortedPriority.forEach((priority, index) => {
const key = `Rule${priority.Rule}`;
rulePriority[key] = index + 1;
});
return rulePriority;
};
const matrix = [];
ruleList.forEach((ruleLine, ruleIndex) => {
ruleLine.forEach((ruleComponent, ordinal) => {
const pty = matrix[ruleIndex] || {};
pty.Rule = ruleIndex + 1;
const outputOrdinal = ordinal - numOfConditions;
if (outputOrdinal < 0) { return; }
const pValue = outputOrdinal < 0 ? -1 : outputValuesList[outputOrdinal].indexOf(ruleComponent);
pty[outputs[outputOrdinal]] = (pValue === -1 ? 0 : (pValue + 1));
matrix[ruleIndex] = pty;
});
});
return calcPriority(matrix, outputs);
}
const createDecisionTree = (dTable) => {
const ruleTree = new Tree('Rule');
const root = ruleTree.root;
const classNodeList = dTable.inputExpressionList;
const numOfConditions = classNodeList.length;
const outputNodeList = dTable.outputs && Array.isArray(dTable.outputs) ? dTable.outputs : [dTable.outputs];
const ruleList = dTable.ruleList;
const outputSet = {};
root.hitPolicy = dTable.hitPolicy;
if (root.hitPolicy === 'P' || root.hitPolicy === 'O') {
if (dTable.priorityList) {
root.priorityList = dTable.priorityList;
} else {
root.priorityList = generatePriorityList(dTable);
}
}
if (dTable.context !== null) {
root.context = new Node(dTable.context, 'Context');
root.context.ast = FEEL(root.context.data);
root.context.children = null;
} else {
root.context = dTable.context;
}
classNodeList.forEach((classValue) => {
const node = new Node(classValue, 'Class');
node.ast = null;
root.children[classValue] = node;
});
ruleList.forEach((row, rowIndex) => {
const outputValue = {};
const index = rowIndex + 1;
const data = `Rule${index}`;
const sentinelNode = new Node(data, 'Sentinel');
sentinelNode.children = null;
row.forEach((cellValue, colIndex) => {
if (colIndex < numOfConditions) {
const node = root.children[classNodeList[colIndex]].children[cellValue] || new Node(cellValue, 'Value');
node.ast = node.ast || FEEL(cellValue, { startRule: 'SimpleUnaryTests' });
node.children[data] = sentinelNode;
root.children[classNodeList[colIndex]].children[cellValue] = root.children[classNodeList[colIndex]].children[cellValue] || node;
} else {
const node = new Node(cellValue);
node.ast = FEEL(cellValue);
node.children = null;
outputValue[outputNodeList[colIndex - numOfConditions]] = node;
}
});
outputSet[data] = outputValue;
});
root.outputSet = outputSet;
return root;
};
const prepareOutput = (outputSet, output, payload) =>
new Promise((resolve, reject) => {
Promise.all(output.map((i) => {
const keys = Object.keys(outputSet[i]);
return new Promise((resolve, reject) => { // eslint-disable-line
Promise.all(keys.map(k => outputSet[i][k].ast.build(payload))).then((results) => {
resolve(results.reduce((res, val, j) => {
const obj = {};
obj[keys[j]] = val;
return Object.assign({}, obj, res);
}, {}));
}).catch(err => reject(err));
});
})).then(results =>
resolve(results)).catch(err => reject(err));
});
const resolveConflictRules = (root, payload, rules) => {
let output = [];
const rootChildren = root.children;
const classArr = Object.keys(rootChildren);
classArr.every((classNode, i) => {
const valueKeys = Object.keys(rootChildren[classNode].children);
const matchKeys = rules[i];
let arr = [];
if (matchKeys.length === 0) {
output = [];
return false;
}
valueKeys.forEach((valKey, j) => {
if (matchKeys.indexOf(j) > -1) {
arr = arr.concat(Object.keys(rootChildren[classNode].children[valKey].children));
}
});
arr = arr.filter((item, index) => arr.indexOf(item) === index);
if (i === 0) {
output = arr;
} else if (output.length > 0) {
output = arr.filter(d => output.indexOf(d) > -1);
} else {
return false;
}
// output = output.length > 0 ? arr.filter(d => output.indexOf(d) > -1) : arr;
return true;
});
if (output.length > 0) {
return prepareOutput(root.outputSet, getOrderedOutput(root, output), payload);
}
return Promise.resolve([]);
};
const traverseDecisionTreeUtil = (root, payload) => {
const classArr = Object.keys(root.children);
return new Promise((resolve, reject) => {
Promise.all(classArr.map((classKey) => {
const node = root.children[classKey];
const sentinelKeys = Object.keys(node.children);
const { decisionMap } = payload;
return new Promise((resolve, reject) => {
const inputExpressionValue = payload[classKey];
if (typeof inputExpressionValue !== 'undefined') {
//! the input expression is resolved from payload
Promise.all(sentinelKeys.map(key => node.children[key].ast.build(payload, {}, 'input'))).then((results) => {
let res = results.map((f, i) => ({ value: f(inputExpressionValue), index: i })).filter(d => d.value === true);
res = res.map(obj => obj.index);
resolve(res);
}).catch(err => reject(err));
} else if (decisionMap[classKey]) {
//! the input expression can be resolved from the decisionMap
const decision = decisionMap[classKey];
decision.build(payload)
.then((value) => {
Promise.all(sentinelKeys.map(key => node.children[key].ast.build(payload, {}, 'input')))
.then((results) => {
let res = results.map((f, i) => ({ value, index: i }))
.filter(d => d.value);
res = res.map(obj => obj.index);
resolve(res);
})
.catch(reject);
})
.catch(reject);
} else {
reject(new Error(`Cannot resolve decision table input expression: ${classKey}`));
}
});
})).then(results =>
resolveConflictRules(root, payload, results).then(results =>
resolve(results))).catch(err => reject(err));
});
};
const prepareContext = (root, payload) => {
if (root.context !== null) {
return root.context.ast.build(payload);
}
return Promise.resolve(payload);
};
const traverseDecisionTree = (root, payload) => new Promise((resolve, reject) => {
prepareContext(root, payload)
.then((context) => {
const ctx = Object.assign({}, payload, context);
return traverseDecisionTreeUtil(root, ctx);
})
.then(results => hitPolicyPass(root.hitPolicy, results))
.then(output => resolve(output))
.catch(err => reject(err));
});
module.exports = {
createTree: createDecisionTree,
traverseTree: traverseDecisionTree,
};