├── .gitattributes ├── img ├── multiDT.png ├── singleDT.png └── exec-tree.PNG ├── test ├── data │ ├── Discount.xlsx │ ├── Holidays.xlsx │ ├── Adjustments.xlsx │ ├── Membership.xlsx │ ├── Validation.xlsx │ ├── Adjustments2.xlsx │ ├── ApplicantData.xlsx │ ├── RoutingRules.xlsx │ ├── RoutingRules2.xlsx │ ├── BillCalculation.xlsx │ ├── CustomerDiscount.xlsx │ ├── ElectricityBill.xlsx │ ├── ExamEligibility.xlsx │ ├── LoanEligibility.xlsx │ ├── ApplicantRiskRating.xlsx │ ├── CustomerDiscount2.xlsx │ ├── empty-output-check.xlsx │ ├── Applicant_Risk_Rating.xlsx │ ├── LoanApplicationValidity.xlsx │ ├── PersonalLoanCompliance.xlsx │ ├── PostBureauRiskCategory.xlsx │ ├── PostBureauRiskCategory2.xlsx │ ├── PostBureauRiskCategory3.xlsx │ ├── RiskCategoryEvaluation.xlsx │ ├── RoutingDecisionService.xlsx │ ├── StudentFinancialPackageEligibility.xlsx │ ├── corrupted-excel-files │ │ └── EmployeeValidation.xlsx │ ├── BoxedExpression-PostBureauRiskCategory-Compressed.txt │ ├── BoxedExpression-PostBureauRiskCategory.txt │ ├── sample2.json │ ├── BoxedExpression-PostBureauRiskCategoryTable-Compressed.txt │ ├── RoutingRules.json │ ├── BoxedExpression-PostBureauRiskCategory2.txt │ ├── BoxedExpression-PostBureauRiskCategoryTable.txt │ └── RoutingDecisionService.json ├── date-time-expression │ ├── misc-date-and-time-related-tests.spec.js │ ├── feel-date-time.build.spec.js │ ├── feel-date.build.spec.js │ ├── feel-time.build.spec.js │ └── feel-duration.build.spec.js ├── decision-table │ ├── decision-table-to-tree.spec.js │ ├── excel-to-decision-table.spec.js │ ├── decision-table-evaluation.spec.js │ └── decision-table-to-feel-parsing.spec.js ├── for-expression │ └── feel-for-expression.spec.js ├── quantified-expression │ └── feel-quantified-expression.spec.js ├── function-builtin │ └── feel-function-builtin.build.spec.js ├── if-expression │ └── feel-if-expression.spec.js ├── decision-service │ ├── servicification-tests.spec.js │ ├── decision-table-tests.spec.js │ ├── decision-table-tests2.spec.js │ ├── feel-invocation-tests.spec.js │ ├── basic-tests.spec.js │ ├── individual-sheets-tests.spec.js │ └── pegjs-tests.spec.js ├── external-function │ ├── external-function-evaluation.spec.js │ └── decision-table-external-function-evaluation.spec.js ├── arithmetic-expression │ └── feel-arithmetic-expression.parse.spec.js ├── function-definition │ ├── feel-function-definition.parse.spec.js │ └── feel-function-definition.build.spec.js ├── context-entry-generator │ └── context-entries-generator-tests.spec.js ├── comparision-expression │ └── feel-comparision-expression.parse.spec.js ├── disjunction-conjunction-expression │ ├── feel-disjunction-conjunction.parse.spec.js │ └── feel-disjunction-conjunction.build.spec.js └── filter-path-expression │ └── filter-path-expression.build.spec.js ├── .eslintignore ├── .gitignore ├── utils ├── built-in-functions │ ├── boolean-functions │ │ ├── index.js │ │ └── not.js │ ├── numbers │ │ └── index.js │ ├── date-time-functions │ │ ├── index.js │ │ ├── misc.js │ │ ├── add-properties.js │ │ ├── date.js │ │ ├── date-time.js │ │ ├── duration.js │ │ └── time.js │ ├── index.js │ ├── decision-table │ │ └── index.js │ ├── strings │ │ └── index.js │ └── list-functions │ │ └── index.js ├── helper │ ├── add-kwargs.js │ ├── external-function.js │ ├── decision-service.js │ ├── meta.js │ ├── name-resolution.js │ ├── value.js │ ├── hit-policy.js │ ├── decision-tree.js │ └── decision-table.js └── dev │ └── gulp-pegjs.js ├── .editorconfig ├── .travis.yml ├── .npmignore ├── .eslintrc ├── settings.js ├── LICENSE ├── CONTRIBUTION.md ├── index.js ├── logger.js ├── package.json ├── grammar └── feel-initializer.js ├── dmn-feel-grammar.txt ├── gulpfile.js ├── INDIVIDUAL_CLA.md ├── CORPORATE_CLA.md └── README.md /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf -------------------------------------------------------------------------------- /img/multiDT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/img/multiDT.png -------------------------------------------------------------------------------- /img/singleDT.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/img/singleDT.png -------------------------------------------------------------------------------- /img/exec-tree.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/img/exec-tree.PNG -------------------------------------------------------------------------------- /test/data/Discount.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Discount.xlsx -------------------------------------------------------------------------------- /test/data/Holidays.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Holidays.xlsx -------------------------------------------------------------------------------- /test/data/Adjustments.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Adjustments.xlsx -------------------------------------------------------------------------------- /test/data/Membership.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Membership.xlsx -------------------------------------------------------------------------------- /test/data/Validation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Validation.xlsx -------------------------------------------------------------------------------- /test/data/Adjustments2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Adjustments2.xlsx -------------------------------------------------------------------------------- /test/data/ApplicantData.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/ApplicantData.xlsx -------------------------------------------------------------------------------- /test/data/RoutingRules.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/RoutingRules.xlsx -------------------------------------------------------------------------------- /test/data/RoutingRules2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/RoutingRules2.xlsx -------------------------------------------------------------------------------- /test/data/BillCalculation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/BillCalculation.xlsx -------------------------------------------------------------------------------- /test/data/CustomerDiscount.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/CustomerDiscount.xlsx -------------------------------------------------------------------------------- /test/data/ElectricityBill.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/ElectricityBill.xlsx -------------------------------------------------------------------------------- /test/data/ExamEligibility.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/ExamEligibility.xlsx -------------------------------------------------------------------------------- /test/data/LoanEligibility.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/LoanEligibility.xlsx -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /grammar/* 2 | /test/* 3 | /dist/* 4 | gulpfile.js 5 | /_archive/* 6 | /utils/dev/* 7 | dummy.js 8 | -------------------------------------------------------------------------------- /test/data/ApplicantRiskRating.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/ApplicantRiskRating.xlsx -------------------------------------------------------------------------------- /test/data/CustomerDiscount2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/CustomerDiscount2.xlsx -------------------------------------------------------------------------------- /test/data/empty-output-check.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/empty-output-check.xlsx -------------------------------------------------------------------------------- /test/data/Applicant_Risk_Rating.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/Applicant_Risk_Rating.xlsx -------------------------------------------------------------------------------- /test/data/LoanApplicationValidity.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/LoanApplicationValidity.xlsx -------------------------------------------------------------------------------- /test/data/PersonalLoanCompliance.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/PersonalLoanCompliance.xlsx -------------------------------------------------------------------------------- /test/data/PostBureauRiskCategory.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/PostBureauRiskCategory.xlsx -------------------------------------------------------------------------------- /test/data/PostBureauRiskCategory2.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/PostBureauRiskCategory2.xlsx -------------------------------------------------------------------------------- /test/data/PostBureauRiskCategory3.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/PostBureauRiskCategory3.xlsx -------------------------------------------------------------------------------- /test/data/RiskCategoryEvaluation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/RiskCategoryEvaluation.xlsx -------------------------------------------------------------------------------- /test/data/RoutingDecisionService.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/RoutingDecisionService.xlsx -------------------------------------------------------------------------------- /test/data/StudentFinancialPackageEligibility.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/StudentFinancialPackageEligibility.xlsx -------------------------------------------------------------------------------- /test/data/corrupted-excel-files/EmployeeValidation.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EdgeVerve/feel/HEAD/test/data/corrupted-excel-files/EmployeeValidation.xlsx -------------------------------------------------------------------------------- /test/data/BoxedExpression-PostBureauRiskCategory-Compressed.txt: -------------------------------------------------------------------------------- 1 | {Post Bureau Risk Category,Existing Customer: Applicant,Credit Score: Report. Credit,Application Risk Score: Affordability Model(Applicant, Product). Application Risk Score} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | snippets 3 | _archive 4 | dummy.js 5 | npm-debug.log 6 | /.vs/feel/v15 7 | .vscode 8 | *.njsproj 9 | *.sln 10 | package-lock.json 11 | 12 | # Coveralls 13 | coverage 14 | 15 | # GitLab 16 | .gitlab-ci.yml -------------------------------------------------------------------------------- /test/data/BoxedExpression-PostBureauRiskCategory.txt: -------------------------------------------------------------------------------- 1 | { 2 | Post Bureau Risk Category, 3 | Existing Customer: Applicant, 4 | Credit Score: Report. Credit, 5 | Application Risk Score: Affordability Model(Applicant, Product). Application Risk Score 6 | } 7 | -------------------------------------------------------------------------------- /utils/built-in-functions/boolean-functions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | module.exports = Object.assign({}, 9 | require('./not')); 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | # editorconfig-tools is unable to ignore longs strings or urls 11 | max_line_length = null -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "16" 4 | sudo: false 5 | cache: 6 | directories: 7 | - node_modules 8 | before_script: 9 | - npm install -g gulp 10 | script: gulp lint && gulp test-ci 11 | after_script: "npm install coveralls@2.10.0 && cat ./coverage/lcov.info | coveralls" 12 | -------------------------------------------------------------------------------- /utils/built-in-functions/numbers/index.js: -------------------------------------------------------------------------------- 1 | const decimal = (n, scale) => { 2 | const pow = 10 ** scale; 3 | return Math.round(n * pow) / pow; 4 | }; 5 | 6 | const floor = n => Math.floor(n); 7 | 8 | const ceiling = n => Math.ceil(n); 9 | 10 | module.exports = { 11 | decimal, 12 | floor, 13 | ceiling, 14 | }; 15 | -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | module.exports = Object.assign({}, 9 | require('./time'), 10 | require('./date-time'), 11 | require('./date'), 12 | require('./duration'), 13 | require('./misc')); 14 | -------------------------------------------------------------------------------- /utils/helper/add-kwargs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | // add new properties to the kwargs object 8 | // returns the updated _args object 9 | module.exports = (_args, obj = {}) => Object.assign({}, _args, { 10 | kwargs: Object.assign({}, _args.kwargs, obj), 11 | }); 12 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #tests 2 | test 3 | coverage 4 | src 5 | grammar 6 | 7 | #build tools 8 | .travis.yml 9 | gulpfile.js 10 | 11 | #linters 12 | .eslintrc 13 | .eslintignore 14 | 15 | #editor settings 16 | .editorconfig 17 | 18 | #markdown 19 | CONTRIBUTION.md 20 | CORPORATE_CLA.md 21 | INDIVIDUAL_CLA.md 22 | 23 | #textfile 24 | dmn-feel-grammar.txt 25 | 26 | #git 27 | .gitattributes 28 | .gitignore 29 | -------------------------------------------------------------------------------- /utils/built-in-functions/boolean-functions/not.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | creates a negation function for any curried function 10 | fn is expected to be a curried function with pre-populated x 11 | fn signature - function(x,y) { // function body } 12 | */ 13 | 14 | const not = fn => y => !fn(y); 15 | 16 | module.exports = { not }; 17 | -------------------------------------------------------------------------------- /test/data/sample2.json: -------------------------------------------------------------------------------- 1 | { 2 | "final dec": "decision table(input expression list : ['test1','name'],outputs : \"outstring\",input values list : [[],[]],output values : [[]],rule list : [['\"Hiii\"','\"Ram\"','\"Hi ram\"'],['\"Hi\"','\"Rajesh\"','\"Hi rajesh\"']],id : 'final dec',hit policy : 'U')", 3 | "test1": "decision table(input expression list : ['msg'],outputs : \"out\",input values list : [[]],output values : [[]],rule list : [['\"Hi\"','\"Hiii\"'],['\"H\"','\"Hi\"']],id : 'test1',hit policy : 'U')" 4 | } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "rules": { 4 | "func-names": ["error", "never"], 5 | "max-len": ["error", { "ignoreTrailingComments": true, "ignoreComments": true, "ignoreStrings": true, "code": 200 }], 6 | "no-param-reassign": ["error", { "props": true, "ignorePropertyModificationsFor": ["ast"] }], 7 | "no-shadow":"off", 8 | "consistent-return": ["error", { "treatUndefinedAsUnspecified": true }], 9 | "no-nested-ternary":"off", 10 | "no-useless-escape":"off", 11 | "no-prototype-builtins":"off" 12 | } 13 | } -------------------------------------------------------------------------------- /test/data/BoxedExpression-PostBureauRiskCategoryTable-Compressed.txt: -------------------------------------------------------------------------------- 1 | {Existing Customer:Applicant. ExistingCustomer,Credit Score:Report. CreditScore,Application Risk Score:Affordability Model(Applicant, Product). Application Risk Score,result:decision table (outputs: "Post Bureau Risk Category Table",input expression list: [Existing Customer,Application Risk Score,Credit Score],rule list: [['TRUE','<=120','<590','"HIGH"'],['TRUE','<=120','[590..610]','"MEDIUM"'],['TRUE','<=120','>610','"LOW"'],['FALSE','<=100','<580','"HIGH"'],['FALSE','<=100','[580..600]','"MEDIUM"'],['FALSE','<=100','>600','"LOW"']],hit policy: "U")} 2 | -------------------------------------------------------------------------------- /test/data/RoutingRules.json: -------------------------------------------------------------------------------- 1 | { 2 | "Routing rules": "decision table(outputs : ['Routing','Review level','Reason'],input expression list : ['Age','Risk category','Debt review'],rule list : [['-','-','-','\"Accept\"','\"None\"','\"Acceptable\"'],['<18','-','-','\"Decline\"','\"None\"','\"Applicant too young\"'],['-','\"High\"','-','\"Refer\"','\"Level1\"','\"High risk application\"'],['-','-','true','\"Refer\"','\"Level2\"','\"Applicant under debt review\"']],id : 'Routing rules',hit policy: \"O\",input values list : [[],['\"Low\"','\"Medium\"','\"High\"'],[]],output values : [['\"Decline\"','\"Refer\"','\"Accept\"'],['\"Level2\"','\"Level1\"','\"None\"'],[]])" 3 | } -------------------------------------------------------------------------------- /utils/built-in-functions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const dateTime = require('./date-time-functions'); 9 | const list = require('./list-functions'); 10 | const boolean = require('./boolean-functions'); 11 | const decisionTable = require('./decision-table'); 12 | const strings = require('./strings'); 13 | const numbers = require('./numbers'); 14 | 15 | const sort = { 16 | sort: (list, precedes) => { 17 | throw new Error('hello world!', list, precedes); 18 | }, 19 | }; 20 | module.exports = Object.assign({}, dateTime, list, boolean, decisionTable, sort, numbers, strings); 21 | 22 | -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/misc.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const moment = require('moment'); 8 | 9 | const { time_ISO_8601, date_ISO_8601 } = require('../../helper/meta'); 10 | 11 | const setTimezone = (obj, timezoneId) => obj.tz(timezoneId); 12 | 13 | const formatDateTime = obj => moment(obj).format(); 14 | 15 | const formatDate = obj => moment(obj).format(date_ISO_8601); 16 | 17 | const formatTime = obj => moment(obj).format(time_ISO_8601); 18 | 19 | const format = (obj, fmt) => obj.format(fmt); 20 | 21 | module.exports = { setTimezone, formatDateTime, formatDate, formatTime, format }; 22 | -------------------------------------------------------------------------------- /test/data/BoxedExpression-PostBureauRiskCategory2.txt: -------------------------------------------------------------------------------- 1 | { 2 | Existing Customer: Applicant. ExistingCustomer, 3 | Credit Score: Report . CreditScore, 4 | Application Risk Score: AffordabilityModel(Applicant, Product) . Application Risk Score 5 | result: decision table ( 6 | outputs: "Post-Bureau Risk Category", 7 | input expression list: [Existing Customer, Application Risk Score, Credit Score] 8 | rule list: [ 9 | ['true', '<=120', '<590', '"HIGH"'], 10 | ['true', '<=120', '[590..610]]', '"MEDIUM"'], 11 | ['true', '<=120', '<590', '"LOW"'], 12 | ['false', '<=100', '<580', '"HIGH"'], 13 | ['false', '<=100', '[580..600]', '"MEDIUM"'], 14 | ['false', '<=100', '>600', '"LOW"'], 15 | ], 16 | hit policy: "Unique") 17 | } 18 | 19 | -------------------------------------------------------------------------------- /test/data/BoxedExpression-PostBureauRiskCategoryTable.txt: -------------------------------------------------------------------------------- 1 | { 2 | Existing Customer: Applicant. ExistingCustomer, 3 | Credit Score: Report . CreditScore, 4 | Application Risk Score: Affordability Model(Applicant, Product) . Application Risk Score 5 | result: decision table ( 6 | outputs: "Post-Bureau Risk Category Table", 7 | input expression list: [Existing Customer, Application Risk Score, Credit Score] 8 | rule list: [ 9 | ['TRUE', '<=120', '<590', '"HIGH"'], 10 | ['TRUE', '<=120', '[590..610]', '"MEDIUM"'], 11 | ['TRUE', '<=120', '>610', '"LOW"'], 12 | ['FALSE', '<=100', '<580', '"HIGH"'], 13 | ['FALSE', '<=100', '[580..600]', '"MEDIUM"'], 14 | ['FALSE', '<=100', '>600', '"LOW"'] 15 | ], 16 | hit policy: "U") 17 | } 18 | 19 | -------------------------------------------------------------------------------- /settings.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /** 9 | * All the library wide settings can be configured in this file. 10 | * This is a collection of the default settings. 11 | */ 12 | // const fs = require('fs'); 13 | 14 | // const logFile = fs.createWriteStream('output2.log'); 15 | 16 | module.exports = { 17 | logger: { 18 | name: 'js-feel', 19 | streams: [ 20 | { 21 | stream: process.stdout, 22 | level: 'info', 23 | }, 24 | // { 25 | // stream: logFile, 26 | // level: 'debug', 27 | // }, 28 | ], 29 | }, 30 | enableLexerLogging: false, 31 | enableExecutionLogging: false, 32 | logResult: false, 33 | }; 34 | -------------------------------------------------------------------------------- /test/date-time-expression/misc-date-and-time-related-tests.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const FEEL = require('../../dist/feel'); 10 | 11 | const expect = chai.expect; 12 | 13 | describe(chalk.blue('misc date-and-time related tests...'), () => { 14 | it('should subtract months from last day of month correctly', (done) => { 15 | debugger; 16 | const text = 'date(date("2018-07-31") - duration("P1M")) = date("2018-06-30")'; 17 | try { 18 | const parsedGrammar = FEEL.parse(text); 19 | parsedGrammar.build() 20 | .then((result) => { 21 | expect(result).to.be.true; 22 | done(); 23 | }).catch((err) => { 24 | done(err); 25 | }); 26 | } catch (err) { 27 | done(err); 28 | } 29 | }); 30 | 31 | }); 32 | -------------------------------------------------------------------------------- /test/decision-table/decision-table-to-tree.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DTable = require('../../utils/helper/decision-table'); 11 | var DTree = require('../../utils/helper/decision-tree'); 12 | var decision_table = {}; 13 | 14 | describe(chalk.blue('Decision table to decision tree'), function () { 15 | 16 | before('setup test data, read excel file and get the decision table', function (done) { 17 | var csv = DTable.xls_to_csv('./test/data/StudentFinancialPackageEligibility.xlsx'); 18 | decision_table = DTable.csv_to_decision_table(csv[0]); 19 | done(); 20 | }); 21 | 22 | it('Successfully converts decision table to decision tree', function (done) { 23 | var root = DTree.createTree(decision_table); 24 | expect(root).not.to.be.null; 25 | done(); 26 | }); 27 | 28 | }); -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2016-2017 EdgeVerve 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /utils/dev/gulp-pegjs.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | 9 | const gutil = require('gulp-util'); 10 | const through = require('through2'); 11 | const pegjs = require('pegjs'); 12 | 13 | module.exports = function (opts) { 14 | return through.obj(function (file, enc, cb) { 15 | if (file.isNull()) { 16 | cb(null, file); 17 | return; 18 | } 19 | 20 | if (file.isStream()) { 21 | cb(new gutil.PluginError('gulp-pegjs', 'Streaming not supported')); 22 | return; 23 | } 24 | 25 | const options = Object.assign({ output: 'source' }, opts); 26 | const filePath = file.path; 27 | 28 | try { 29 | file.contents = new Buffer(pegjs.generate(file.contents.toString(), options)); 30 | file.path = gutil.replaceExtension(file.path, '.js'); 31 | this.push(file); 32 | } catch (err) { 33 | this.emit('error', new gutil.PluginError('gulp-pegjs', err, { fileName: filePath })); 34 | } 35 | 36 | cb(); 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /utils/built-in-functions/decision-table/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const { execute_decision_table: execDTable } = require('../../helper/decision-table'); 8 | 9 | const decisionTable = (args, tableData) => { 10 | const { 11 | id, 12 | 'input expression list': inputExpressionList, 13 | 'input values list': inputValuesList, 14 | outputs, 15 | 'output values': outputValues, 16 | 'rule list': ruleList, 17 | 'hit policy': hitPolicy, 18 | 'default output value': defaultOutputValue, 19 | } = tableData; 20 | 21 | const context = null; 22 | 23 | return new Promise((resolve, reject) => { 24 | execDTable(id, { context, inputExpressionList, inputValuesList, outputs, outputValues, ruleList, hitPolicy, defaultOutputValue }, args, (err, result) => { 25 | if (err) { 26 | reject(err); 27 | } else { 28 | resolve(result); 29 | } 30 | }); 31 | }); 32 | }; 33 | 34 | module.exports = { 'decision table': decisionTable }; 35 | -------------------------------------------------------------------------------- /CONTRIBUTION.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Thank you for your interest in oe.io, an open source project administered by EdgeVerve. 4 | 5 | 6 | ## Raising issues 7 | 8 | Please raise any bug reports on the relevant project's issue tracker. Be sure to 9 | search the list to see if your issue has already been raised. 10 | 11 | A good bug report is one that make it easy for us to understand what you were 12 | trying to do and what went wrong. 13 | 14 | 15 | ### Contributor License Agreement 16 | 17 | You must sign a Contributor License Agreement (CLA) before submitting your pull request. To complete the CLA, please email a signed .pdf file of this Agreement to IPC@EdgeVerve.com. You need to complete the CLA only once to cover all EdgeVerve projects. 18 | 19 | You can download the CLAs here: 20 | - [Individual Contributer License Agreement](./INDIVIDUAL_CLA.md) 21 | - [Corporate Contributer License Agreement](./CORPORATE_CLA.md) 22 | 23 | If you are an Infosys employee, please contact us directly as the contribution process is 24 | slightly different. 25 | 26 | ### Coding standards 27 | 28 | Please ensure you follow the coding standards used through-out the existing code base. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const decisionTable = require('./utils/helper/decision-table'); 9 | const decisionLogic = require('./utils/helper/decision-logic'); 10 | const feel = require('./dist/feel'); 11 | const decisionService = require('./utils/helper/decision-service'); 12 | const { configureLogger } = require('./logger'); 13 | const feelSettings = require('./settings'); 14 | 15 | const jsFeel = { 16 | decisionTable, 17 | feel, 18 | decisionLogic, 19 | decisionService, 20 | }; 21 | 22 | jsFeel.init = function (settings) { 23 | const { logger, enableLexerLogging, enableExecutionLogging, logResult } = settings; 24 | configureLogger(logger); 25 | if (enableExecutionLogging !== undefined) { 26 | feelSettings.enableExecutionLogging = enableExecutionLogging; 27 | } 28 | if (enableLexerLogging !== undefined) { 29 | feelSettings.enableLexerLogging = enableLexerLogging; 30 | } 31 | if (logResult !== undefined) { 32 | feelSettings.logResult = logResult; 33 | } 34 | }; 35 | 36 | jsFeel.use = function (plugin) { 37 | plugin.call(this); 38 | }; 39 | 40 | module.exports = () => jsFeel; 41 | -------------------------------------------------------------------------------- /logger.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /** 9 | * Creates a bunyan logger if no custom logger is passed to the configureLogger utility function. 10 | */ 11 | 12 | const bunyan = require('bunyan'); 13 | const { logger: loggerSettings } = require('./settings'); 14 | 15 | let logger = (name) => { 16 | const settings = Object.assign({}, loggerSettings, { name }); 17 | const defaultLogger = bunyan.createLogger(settings); 18 | const levels = ['trace', 'info', 'warn', 'error', 'debug', 'fatal', 'ast']; 19 | const modifiedLogger = {}; 20 | levels.forEach((level) => { 21 | modifiedLogger[level] = (...args) => { 22 | let _; // eslint-disable-line no-unused-vars 23 | let message; 24 | if (args.length === 2) { 25 | [_, message] = args; 26 | } else if (args.length === 1) { 27 | [message] = args; 28 | } 29 | defaultLogger[level](message); 30 | }; 31 | }); 32 | return modifiedLogger; 33 | }; 34 | 35 | const configureLogger = (customLogger) => { 36 | logger = customLogger || logger; 37 | }; 38 | 39 | module.exports = { configureLogger, logger: name => logger(name) }; 40 | -------------------------------------------------------------------------------- /test/decision-table/excel-to-decision-table.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DTable = require('../../utils/helper/decision-table'); 11 | 12 | describe(chalk.blue('Excel to decision table conversion test'), function () { 13 | 14 | it('Parse excel and convert it to csv format', function (done) { 15 | var csv = DTable.xls_to_csv('./test/data/StudentFinancialPackageEligibility.xlsx'); 16 | var decision_table = DTable.csv_to_decision_table(csv[0]); 17 | 18 | expect(decision_table.hitPolicy).to.equal('R'); 19 | expect(decision_table.inputExpressionList.length).to.equal(3); 20 | expect(decision_table.inputValuesList[0].split(",").length).to.equal(3); 21 | expect(decision_table.inputValuesList[1].split(",").length).to.equal(3); 22 | expect(decision_table.inputValuesList[2].split(",").length).to.equal(3); 23 | expect(decision_table.outputs.length).to.equal(1); 24 | expect(decision_table.outputValues[0].length).to.equal(4); 25 | expect(decision_table.ruleList.length).to.equal(4); 26 | done(); 27 | }); 28 | 29 | }); -------------------------------------------------------------------------------- /utils/helper/external-function.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const vm = require('vm'); 9 | 10 | const callback = (resolve, reject) => (err, res) => { 11 | if (err) { 12 | reject(err); 13 | } else { 14 | resolve(res); 15 | } 16 | }; 17 | 18 | const execute = (script, payload, done) => { 19 | const sandbox = Object.assign({}, payload); 20 | sandbox.done = done; 21 | script.runInNewContext(sandbox); 22 | }; 23 | 24 | const prepareDependencies = (dependencies) => { 25 | const requireObj = {}; 26 | dependencies.forEach((dependency) => { 27 | Object.keys(dependency).forEach((key) => { 28 | requireObj[key] = require(dependency[key]); // eslint-disable-line 29 | }); 30 | }); 31 | return requireObj; 32 | }; 33 | 34 | const externalFn = bodyMeta => ((code, dependencies) => { 35 | const script = new vm.Script(code); 36 | const reqdLibs = Object.assign({}, prepareDependencies(dependencies), global); 37 | return (payload, done) => execute(script, Object.assign({}, reqdLibs, payload), done); 38 | })(bodyMeta.js.signature || '', bodyMeta.js.dependencies || []); 39 | 40 | module.exports = (ctx, bodyMeta) => new Promise((resolve, reject) => { 41 | externalFn(bodyMeta)(ctx, callback(resolve, reject)); 42 | }); 43 | -------------------------------------------------------------------------------- /test/for-expression/feel-for-expression.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('For expression grammar test'), function () { 13 | 14 | it('Successfully creates ast from for expression', function (done) { 15 | var text = 'for a in [1,2,3] return a * a'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Successfully creates ast from for expression', function (done) { 28 | var text = 'for age in [18..40], name in ["george", "mike", "bob"] return status'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).not.to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).not.to.be.undefined; 35 | expect(e).to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | 40 | }); -------------------------------------------------------------------------------- /test/quantified-expression/feel-quantified-expression.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Quantified expression grammar test'), function () { 13 | 14 | it('Successfully creates ast from simple quantified expression', function (done) { 15 | var text = 'some ch in credit history satisfies ch.event = "bankruptcy"'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Fails to create ast from quantified expression', function (done) { 28 | var text = 'somech in credit history satisfies ch.event = "bankruptcy"'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).to.be.undefined; 35 | expect(e).not.to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | }); -------------------------------------------------------------------------------- /utils/helper/decision-service.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const FEEL = require('../../dist/feel.js'); 8 | 9 | // const functionDeclarationToken = 'function() '; 10 | 11 | // const massageDecisionMap = decisionMap => Object.keys(decisionMap).reduce((recur, next) => { 12 | // const value = decisionMap[next]; 13 | // const r = recur; 14 | // r[next] = typeof value === 'string' && value && (functionDeclarationToken + value); 15 | // return r; 16 | // }, {}); 17 | 18 | const createDecisionGraphAST = (decisionMap) => { 19 | const graphAST = Object.keys(decisionMap).reduce((recur, next) => { 20 | const value = decisionMap[next]; 21 | const r = recur; 22 | r[next] = FEEL.parse(value, { ruleName: next }); 23 | return r; 24 | }, {}); 25 | return graphAST; 26 | }; 27 | 28 | const executeDecisionService = (graphAST, decisionName, payload, graphName) => 29 | new Promise((resolve, reject) => { 30 | const decision = graphAST[decisionName]; 31 | if (decision) { 32 | decision 33 | .build(payload, { decisionMap: graphAST, graphName }) 34 | .then((result) => { 35 | resolve(result); 36 | }) 37 | .catch(err => reject(err)); 38 | } 39 | }); 40 | 41 | module.exports = { createDecisionGraphAST, executeDecisionService }; 42 | -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/add-properties.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const addProperties = (obj, props) => { 9 | const child = Object.create(obj); 10 | Object.keys(props).forEach((key) => { 11 | const value = props[key]; 12 | if (typeof value === 'function') { 13 | Object.defineProperty(child, key, { get: function () { // eslint-disable-line object-shorthand 14 | const proto = Object.getPrototypeOf(this); 15 | return value.call(proto); 16 | }, 17 | }); 18 | } else { 19 | Object.defineProperty(child, key, { get: function () { // eslint-disable-line object-shorthand 20 | const proto = Object.getPrototypeOf(this); 21 | return key !== 'type' && proto[value] ? proto[value]() : value; 22 | }, 23 | }); 24 | } 25 | }); 26 | 27 | const proxy = new Proxy(child, { 28 | get: (target, propKey) => { 29 | const proto = Object.getPrototypeOf(target); 30 | const protoPropValue = proto[propKey]; 31 | if (!target.hasOwnProperty(propKey) && typeof protoPropValue === 'function') { 32 | return function (...args) { 33 | return protoPropValue.apply(proto, args); 34 | }; 35 | } 36 | return target[propKey]; 37 | }, 38 | }); 39 | 40 | return proxy; 41 | }; 42 | 43 | module.exports = addProperties; 44 | -------------------------------------------------------------------------------- /test/date-time-expression/feel-date-time.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const FEEL = require('../../dist/feel'); 10 | 11 | const expect = chai.expect; 12 | 13 | describe(chalk.blue('date and time built-in function grammar test'), () => { 14 | it('should parse date and time with format "YYYY-MM-DDTHH:mm:ssZ"', (done) => { 15 | debugger; 16 | const text = 'date and time("2012-12-24T23:59:00").isDateTime'; 17 | try { 18 | const parsedGrammar = FEEL.parse(text); 19 | parsedGrammar.build() 20 | .then((result) => { 21 | expect(result).to.be.true; 22 | done(); 23 | }).catch((err) => { 24 | done(err); 25 | }); 26 | } catch (err) { 27 | done(err); 28 | } 29 | }); 30 | 31 | it('should parse date and time with format "([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})(?:@(.+))+"', (done) => { 32 | const text = 'date and time("2012-12-24T00:01:00@Etc/UTC").isDateTime'; 33 | try { 34 | const parsedGrammar = FEEL.parse(text); 35 | parsedGrammar.build() 36 | .then((result) => { 37 | expect(result).to.be.true; 38 | done(); 39 | }).catch((err) => { 40 | done(err); 41 | }); 42 | } catch (err) { 43 | done(err); 44 | } 45 | }); 46 | }); 47 | -------------------------------------------------------------------------------- /utils/built-in-functions/strings/index.js: -------------------------------------------------------------------------------- 1 | // const substring = (text, start, length) => length ? text.substr(start - 1, length) : text.substr(start - 1); // eslint-disable-line no-confusing-arrow 2 | const substring = (text, start, length) => { 3 | if (length && start > -1) { 4 | return text.substr(start - 1, length); 5 | } else if (length && start <= -1) { 6 | return text.substr(start, length); 7 | } else if (start > -1) { 8 | return text.substr(start - 1); 9 | } 10 | 11 | return text.substr(start); 12 | }; 13 | const stringLength = text => text.length; 14 | 15 | const upperCase = text => text.toUpperCase(); 16 | 17 | const lowerCase = text => text.toLowerCase(); 18 | 19 | const substringBefore = (text, match) => { 20 | const idx = text.indexOf(match); 21 | return idx !== -1 ? text.substring(0, idx) : ''; 22 | }; 23 | 24 | const substringAfter = (text, match) => { 25 | const idx = text.indexOf(match); 26 | return idx !== -1 ? text.substring(idx + match.length) : ''; 27 | }; 28 | 29 | const replace = (input, pattern, replacement, flags) => { 30 | const regEx = new RegExp(pattern, flags); 31 | return input.replace(regEx, replacement); 32 | }; 33 | 34 | const contains = (text, match) => text.indexOf(match) > -1; 35 | 36 | const startsWith = (text, match) => text.startsWith(match); 37 | 38 | const endsWith = (text, match) => text.endsWith(match); 39 | 40 | const matches = (text, pattern, flags) => { 41 | const rgx = new RegExp(pattern, flags); 42 | return rgx.test(text); 43 | }; 44 | 45 | module.exports = { 46 | substring, 47 | 'string length': stringLength, 48 | 'upper case': upperCase, 49 | 'lower case': lowerCase, 50 | 'substring before': substringBefore, 51 | 'substring after': substringAfter, 52 | replace, 53 | contains, 54 | 'starts with': startsWith, 55 | 'ends with': endsWith, 56 | matches, 57 | }; 58 | -------------------------------------------------------------------------------- /utils/helper/meta.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | Metadata for parsing the date() { return this }, time, date_and_time and duration for various supported formats. 10 | Contains the properties which needs to be added to the date, time, date_and_time and duration objects 11 | as per the specification defined in 12 | "Table 53: Specific semantics of date, time and duration properties". 13 | 14 | Decision Model and Notation, v1.1. 15 | Page : 126 16 | */ 17 | 18 | /* 19 | Note : 20 | As some of the moment functions are overwritten with properties, native functions like moment.format() might not work. 21 | In that case format function should also be overwritten to suit the requirements 22 | */ 23 | 24 | const metadata = { 25 | defaultTz: 'Etc/UTC', 26 | UTC: 'Etc/UTC', 27 | epoch: '1970-01-01', 28 | UTCTimePart: 'T00:00:00Z', 29 | time_ISO_8601: 'THH:mm:ssZ', 30 | date_ISO_8601: 'YYYY-MM-DD', 31 | time_IANA_tz: /([0-9]{2}):([0-9]{2}):([0-9]{2})(?:@(.+))+/, 32 | date_time_IANA_tz: /([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})(?:@(.+))+/, 33 | ymd_ISO_8601: /P([0-9]+Y)?([0-9]+M)?/, 34 | dtd_ISO_8601: /P([0-9]+D)?(T([0-9]+H)?([0-9]+M)?([0-9]+S)?)?/, 35 | types: { 36 | time: 'time', 37 | date: 'date', 38 | date_and_time: 'date_and_time', 39 | ymd: 'ymd', 40 | dtd: 'dtd', 41 | }, 42 | properties: { 43 | year: 'year', 44 | month: 'month', 45 | day: 'date', 46 | hour: 'hour', 47 | minute: 'minute', 48 | second: 'second', 49 | 'time offset': function () { return this.format('Z'); }, 50 | timezone: 'tz', 51 | years: 'years', 52 | months: 'months', 53 | days: 'days', 54 | hours: 'hours', 55 | minutes: 'minutes', 56 | seconds: 'seconds', 57 | }, 58 | }; 59 | 60 | module.exports = metadata; 61 | -------------------------------------------------------------------------------- /test/function-builtin/feel-function-builtin.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | �2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), Bangalore, India. All Rights Reserved. 3 | The EdgeVerve proprietary software program ("Program"), is protected by copyrights laws, international treaties and other pending or existing intellectual property rights in India, the United States and other countries. 4 | The Program may contain/reference third party or open source components, the rights to which continue to remain with the applicable third party licensors or the open source community as the case may be and nothing here transfers the rights to the third party and open source components, except as expressly permitted. 5 | Any unauthorized reproduction, storage, transmission in any form or by any means (including without limitation to electronic, mechanical, printing, photocopying, recording or otherwise), or any distribution of this Program, or any portion of it, may result in severe civil and criminal penalties, and will be prosecuted to the maximum extent possible under the law. 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Builtin function grammar test'), function() { 13 | 14 | describe(chalk.blue('decimal function'), function() { 15 | 16 | it('Successfully creates nested expression', function(done) { 17 | var text = 'decimal(decimal(0.22 * a, 0) * 223.65, 0)'; 18 | const context = { 19 | a: 10 20 | }; 21 | 22 | var parsedGrammar = FEEL.parse(text); 23 | parsedGrammar.build(context).then(result => { 24 | expect(result).not.to.be.undefined; 25 | expect(result).to.be.equal(447); 26 | done(); 27 | }).catch(err => done(err)); 28 | }); 29 | }); 30 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-feel", 3 | "version": "1.4.7", 4 | "description": "FEEL(Friendly Enough Expression Language) based on DMN specification 1.1 for conformance level 3", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint .", 8 | "lintfix": "eslint . --fix", 9 | "test": "./node_modules/.bin/mocha ./test/**/*.spec.js", 10 | "build": "./node_modules/.bin/gulp", 11 | "precommit-msg": "echo 'Pre-commit checks...' && exit 0" 12 | }, 13 | "pre-commit": [ 14 | "precommit-msg", 15 | "lint" 16 | ], 17 | "engines": { 18 | "node": ">=6.9.2" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git@github.com:EdgeVerve/feel.git" 23 | }, 24 | "keywords": [ 25 | "FEEL", 26 | "DMN 1.1", 27 | "Expression", 28 | "Language", 29 | "PEG", 30 | "PEGjs", 31 | "Decision Table", 32 | "Rule Engine" 33 | ], 34 | "author": "Pragyan Das ", 35 | "dependencies": { 36 | "big.js": "3.2.0", 37 | "bunyan": "1.8.13", 38 | "lodash": "4.17.21", 39 | "moment": "2.29.4", 40 | "moment-timezone": "0.5.27", 41 | "xlsx": "0.17.0" 42 | }, 43 | "devDependencies": { 44 | "chai": "3.4.1", 45 | "chalk": "1.1.1", 46 | "eslint": "4.10.0", 47 | "eslint-config-airbnb": "14.1.0", 48 | "eslint-plugin-import": "2.20.0", 49 | "eslint-plugin-jsx-a11y": "4.0.0", 50 | "eslint-plugin-react": "6.10.3", 51 | "gulp": "4.0.2", 52 | "gulp-clean": "0.3.2", 53 | "gulp-concat": "2.6.1", 54 | "gulp-eslint": "3.0.1", 55 | "gulp-if": "2.0.2", 56 | "gulp-insert": "0.5.0", 57 | "gulp-istanbul": "1.1.3", 58 | "gulp-mocha": "7.0.2", 59 | "gulp-util": "3.0.8", 60 | "istanbul": "0.4.5", 61 | "minimist": "1.2.0", 62 | "mocha": "5.2.0", 63 | "pegjs": "0.10.0", 64 | "pegjs-backtrace": "0.1.2", 65 | "pre-commit": "1.2.2", 66 | "through2": "2.0.5" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/if-expression/feel-if-expression.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('IF expression grammar test'), function () { 13 | 14 | it('Successfully creates ast from simple if expression', function (done) { 15 | var text = 'if applicant.maritalStatus in ("M", "S") then "valid" else "not valid"'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Successfully creates ast from if expression', function (done) { 28 | var text = 'if Pre-Bureau Risk Category = "DECLINE" or Installment Affordable = false or Age < 18 or Monthly Income < 100 then "INELIGIBLE" else "ELIGIBLE"'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).not.to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).not.to.be.undefined; 35 | expect(e).to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | 40 | it('Successfully creates ast from if expression', function (done) { 41 | var text = 'if "Pre-Bureau Risk Category" = "DECLINE" or "Installment Affordable" = false or Age < 18 or "Monthly Income" < 100 then "INELIGIBLE" else "ELIGIBLE"'; 42 | 43 | try { 44 | var parsedGrammar = FEEL.parse(text); 45 | expect(parsedGrammar).not.to.be.undefined; 46 | } catch (e) { 47 | expect(parsedGrammar).not.to.be.undefined; 48 | expect(e).to.be.undefined; 49 | } 50 | done(); 51 | }); 52 | 53 | }); -------------------------------------------------------------------------------- /test/decision-service/servicification-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var XLSX = require('xlsx'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DL = require('../../utils/helper/decision-logic'); 11 | var fs = require('fs'); 12 | 13 | var testFile = 'test/data/RoutingDecisionService.xlsx'; 14 | 15 | describe('servicification tests...', function() { 16 | it('should be that each worksheet should not have blank rows at start', function() { 17 | var { parseXLS } = DL._; 18 | 19 | var parsed = parseXLS(testFile); 20 | 21 | var regex = /^(&SP)+&RSP/; 22 | 23 | parsed.forEach( sheetHash => { 24 | var key = Object.keys(sheetHash)[0]; 25 | var csv = sheetHash[key]; 26 | 27 | expect(regex.test(csv), key + ' sheet is faulty').to.be.false; 28 | }); 29 | }); 30 | 31 | it('should be that each worksheet has a proper qualified name', function() { 32 | var { parseXLS } = DL._; 33 | 34 | var parsed = parseXLS(testFile); 35 | 36 | parsed.forEach( sheetHash => { 37 | var key = Object.keys(sheetHash)[0]; 38 | var csv = sheetHash[key]; 39 | var qn = csv.substring(0, csv.indexOf('&SP')) 40 | // expect(regex.test(csv), key + ' sheet is faulty').to.be.false; 41 | expect(qn).to.be.string; 42 | expect(qn.length, 'Could not detect qualified name for sheet: ' + key).to.not.equal(0) 43 | }); 44 | }); 45 | // became defunct 46 | // it('should expose a json-feel object which exposes a service', function() { 47 | // debugger; 48 | // var jsonFeel = DL.parseWorkbook(testFile); 49 | 50 | // expect(jsonFeel).to.be.defined; 51 | // expect(jsonFeel).to.be.object; 52 | 53 | // var workbook = XLSX.readFile(testFile); 54 | 55 | // expect(workbook.SheetNames.length + 1).to.equal(Object.keys(jsonFeel).length) 56 | // expect(jsonFeel._services).to.be.defined; 57 | 58 | // expect(jsonFeel._services).to.eql(['Routing']); 59 | 60 | // }); 61 | 62 | 63 | }); 64 | -------------------------------------------------------------------------------- /test/external-function/external-function-evaluation.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const expect = chai.expect; 10 | const FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('External function definition and evaluation'), () => { 13 | 14 | before('setup test data, read excel file and get the decision table', (done) => { 15 | done(); 16 | }); 17 | 18 | it('External Function - Evaluation', (done) => { 19 | const context = '{ foo : function(a, b) external { js : {dependencies : [{ _ : "lodash"}], signature : "done(null, _.chunk(a, b))"}}}'; 20 | const text = 'foo(a,b)' 21 | const parsedContext = FEEL.parse(context); 22 | const parsedText = FEEL.parse(text); 23 | const payload = {a:[1,2,3,4], b: 2}; 24 | 25 | parsedContext.build(payload).then((ctx) => { 26 | return parsedText.build(Object.assign({}, ctx, payload)); 27 | }).then((result) => { 28 | expect(result).not.to.be.undefined; 29 | expect(result.length).to.equal(2); 30 | expect(result[0].length).to.equal(2); 31 | expect(result[1].length).to.equal(2); 32 | done(); 33 | }).catch(err => { 34 | done(err); 35 | }); 36 | }); 37 | 38 | it('External Function - Evaluation', (done) => { 39 | const context = '{ foo : function(a, b) external { js : {signature : "setTimeout(done, b, null, a)"}}}'; 40 | const text = 'foo(a,b)' 41 | const parsedContext = FEEL.parse(context); 42 | const parsedText = FEEL.parse(text); 43 | const payload = {a: 5, b: 1000}; 44 | 45 | parsedContext.build(payload).then((ctx) => { 46 | return parsedText.build(Object.assign({}, ctx, payload)); 47 | }).then((result) => { 48 | expect(result).to.equal(payload.a); 49 | done(); 50 | }).catch(err => { 51 | done(err); 52 | }); 53 | }); 54 | 55 | }); 56 | -------------------------------------------------------------------------------- /test/arithmetic-expression/feel-arithmetic-expression.parse.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Arithmetic expression grammar test'), function() { 13 | 14 | it('Successfully creates ast from simple arithmetic expression', function(done) { 15 | var text = 'a + b - c'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Successfully creates ast from arithmetic expression', function(done) { 28 | var text = '((a + b)/c - (d + e*2))**f'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).not.to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).not.to.be.undefined; 35 | expect(e).to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | 40 | it('Successfully creates ast from arithmetic expression', function(done) { 41 | var text = '1-(1+rate/12)**-term'; 42 | 43 | try { 44 | var parsedGrammar = FEEL.parse(text); 45 | expect(parsedGrammar).not.to.be.undefined; 46 | } catch (e) { 47 | expect(parsedGrammar).not.to.be.undefined; 48 | expect(e).to.be.undefined; 49 | } 50 | done(); 51 | }); 52 | 53 | it('Successfully creates ast from arithmetic expression', function(done) { 54 | var text = '(a + b)**-c'; 55 | 56 | try { 57 | var parsedGrammar = FEEL.parse(text); 58 | expect(parsedGrammar).not.to.be.undefined; 59 | } catch (e) { 60 | expect(parsedGrammar).not.to.be.undefined; 61 | expect(e).to.be.undefined; 62 | } 63 | done(); 64 | }); 65 | 66 | }); -------------------------------------------------------------------------------- /utils/helper/name-resolution.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const nameResolutionOrder = ['kwargs', 'context', 'decisionMap', 'plugin']; 9 | 10 | const resolveName = (name, args, isResult = false) => 11 | new Promise((resolve, reject) => { 12 | nameResolutionOrder.some((key, index) => { 13 | let value; 14 | if (key === 'plugin') { 15 | value = 16 | args.context && args.context.plugin && args.context.plugin[name]; 17 | } else { 18 | value = args[key] && args[key][name]; 19 | } 20 | 21 | if (typeof value !== 'undefined') { 22 | if (key === 'kwargs' || key === 'context') { 23 | resolve(value); 24 | } else if (key === 'decisionMap') { 25 | if (!isResult) { 26 | value 27 | .build(Object.assign({}, args.context, args.kwargs), { 28 | decisionMap: args.decisionMap, 29 | plugin: args.plugin, 30 | }) 31 | .then((result) => { 32 | const decisionValue = typeof result === 'object' 33 | ? Object.keys(result).map(key => result[key])[0] 34 | : result; 35 | resolve(decisionValue); 36 | }); 37 | } else { 38 | const decision = { 39 | expr: value, 40 | isDecision: true, 41 | }; 42 | resolve(decision); 43 | } 44 | } else if (key === 'plugin') { 45 | if (typeof value === 'function') { 46 | // Assumption: functions added to plugins return a promise 47 | value() 48 | .then((result) => { 49 | resolve({ context: result }); 50 | }) 51 | .catch((err) => { 52 | reject(err); 53 | }); 54 | } else { 55 | resolve(value); 56 | } 57 | } 58 | return true; 59 | } 60 | if (index === nameResolutionOrder.length - 1) { 61 | resolve(value); 62 | } 63 | return false; 64 | }); 65 | }); 66 | 67 | module.exports = resolveName; 68 | -------------------------------------------------------------------------------- /test/date-time-expression/feel-date.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const FEEL = require('../../dist/feel'); 10 | 11 | const expect = chai.expect; 12 | 13 | describe(chalk.blue('date built-in function grammar test'), () => { 14 | it('should parse date with format "YYYY-MM-DD"', (done) => { 15 | const text = 'date("2017-06-10").isDate'; 16 | try { 17 | const parsedGrammar = FEEL.parse(text); 18 | parsedGrammar.build() 19 | .then((result) => { 20 | expect(result).to.be.true; 21 | done(); 22 | }).catch((err) => { 23 | done(err); 24 | }); 25 | } catch (err) { 26 | done(err); 27 | } 28 | }); 29 | 30 | it('should extract year part from date', (done) => { 31 | const text = 'date("2017-06-10").year'; 32 | try { 33 | const parsedGrammar = FEEL.parse(text); 34 | parsedGrammar.build() 35 | .then((result) => { 36 | expect(result).to.equal(2017); 37 | done(); 38 | }).catch((err) => { 39 | done(err); 40 | }); 41 | } catch (err) { 42 | done(err); 43 | } 44 | }); 45 | 46 | it('should extract month part from date', (done) => { 47 | const text = 'date("2017-06-10").month'; 48 | try { 49 | const parsedGrammar = FEEL.parse(text); 50 | parsedGrammar.build() 51 | .then((result) => { 52 | expect(result).to.equal(5); 53 | done(); 54 | }).catch((err) => { 55 | done(err); 56 | }); 57 | } catch (err) { 58 | done(err); 59 | } 60 | }); 61 | 62 | it('should extract day part from date', (done) => { 63 | const text = 'date("2017-06-10").day'; 64 | try { 65 | const parsedGrammar = FEEL.parse(text); 66 | parsedGrammar.build() 67 | .then((result) => { 68 | expect(result).to.equal(10); 69 | done(); 70 | }).catch((err) => { 71 | done(err); 72 | }); 73 | } catch (err) { 74 | done(err); 75 | } 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /test/decision-service/decision-table-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var XLSX = require('xlsx'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DL = require('../../utils/helper/decision-logic'); 11 | var fs = require('fs'); 12 | 13 | var testDataFile = 'test/data/RoutingDecisionService.xlsx'; 14 | var testDataFile2 = 'test/data/ApplicantData.xlsx'; 15 | 16 | describe('basic tests...', function() { 17 | // defunct - we are separately specifying this one 18 | // it('should detect a sheet marked to be exposed as decision service', function() { 19 | // var workbook = XLSX.readFile(testDataFile); 20 | 21 | // var worksheet = workbook.Sheets["Routing"]; 22 | 23 | // var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 24 | 25 | // var result = DL._.isDecisionService(csvExcel); 26 | 27 | // expect(result).to.equal(true) 28 | // }); 29 | 30 | it('should detect a sheet marked as a boxed invocation', function() { 31 | var workbook = XLSX.readFile(testDataFile); 32 | 33 | var worksheet = workbook.Sheets["Post-Bureau risk category"]; 34 | 35 | var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 36 | 37 | var result = DL._.isBoxedInvocation(csvExcel); 38 | 39 | expect(result).to.equal(true); 40 | }); 41 | 42 | it('should detect a sheet marked as a boxed context with result', function() { 43 | var workbook = XLSX.readFile(testDataFile); 44 | 45 | var worksheet = workbook.Sheets["Installment Calculation"]; 46 | 47 | var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 48 | 49 | var result = DL._.isBoxedContextWithResult(csvExcel); 50 | 51 | expect(result).to.be.true; 52 | 53 | }); 54 | 55 | it('should detect a sheet marked as a boxed context without result', function() { 56 | var workbook = XLSX.readFile(testDataFile2); 57 | 58 | var worksheet = workbook.Sheets["Applicant Data"]; 59 | 60 | var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 61 | 62 | var result = DL._.isBoxedContextWithoutResult(csvExcel); 63 | 64 | expect(result).to.be.true; 65 | 66 | }); 67 | 68 | }); 69 | -------------------------------------------------------------------------------- /test/decision-service/decision-table-tests2.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var fs = require('fs'); 11 | var { createDecisionGraphAST, executeDecisionService } = require('../../index')().decisionService; 12 | 13 | var readJSON = (file) => JSON.parse(fs.readFileSync(file, { encoding: 'utf8' })); 14 | 15 | describe('decision table exposed in a service...', function () { 16 | it('should properly execute a decision table with is exposed through a service and one of its input expression references something in the graph', done => { 17 | var decisionMap = readJSON('./test/data/sample2.json'); 18 | var ast = createDecisionGraphAST(decisionMap); 19 | var payload = { 20 | "name": "Ram", 21 | "msg": "Hi" 22 | } 23 | 24 | executeDecisionService(ast, 'final dec', payload, 'foo') 25 | .then(result => { 26 | expect(result).to.be.object; 27 | expect(result).to.have.property('outstring'); 28 | expect(result.outstring).to.equal('Hi ram'); 29 | done(); 30 | }). 31 | catch(done); 32 | }); 33 | 34 | it('should error correctly when a decision table with a non-existent input expression is given', done => { 35 | var decisionMap = readJSON('./test/data/sample2.json'); 36 | 37 | var feelString = decisionMap['final dec']; 38 | 39 | decisionMap['final dec'] = feelString.replace('test1', 'test2'); 40 | expect(feelString.substr(feelString.indexOf('test'), 5)).to.equal('test1'); 41 | var feelString2 = decisionMap['final dec']; 42 | expect(feelString2.substr(feelString2.indexOf('test'), 5)).to.equal('test2'); 43 | var ast = createDecisionGraphAST(decisionMap); 44 | 45 | var payload = { 46 | "name": "Ram", 47 | "msg": "Hi" 48 | }; 49 | 50 | executeDecisionService(ast, 'final dec', payload, 'foo2') 51 | 52 | .then(() => { 53 | done(new Error('should not execute')); 54 | }) 55 | .catch(err => { 56 | expect(err.message).to.include('test2'); 57 | done(); 58 | }); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /utils/helper/value.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | Decision Model and Notation, v1.1 10 | Page : 112 - 113 11 | value and valueInverse functions for each of the time, date, date_and_time and duration types. 12 | These functions are not exposed as a part of in-built function suite. 13 | These are used for performing calculations and conversions. 14 | */ 15 | 16 | const moment = require('moment-timezone'); 17 | const { time, 'date and time': dateAndTime, duration } = require('../built-in-functions'); 18 | const { date_ISO_8601, time_ISO_8601, epoch } = require('./meta'); 19 | 20 | const prepareTime = (value, offset) => { 21 | let remainingTime = value; 22 | const hour = Math.floor(remainingTime / 3600); 23 | remainingTime = value % 3600; 24 | const minute = Math.floor(remainingTime / 60); 25 | remainingTime = value % 60; 26 | const second = remainingTime; 27 | 28 | return moment.parseZone(`${moment({ hour, minute, second }).format('THH:mm:ss')}${offset}`, time_ISO_8601).format(time_ISO_8601); 29 | }; 30 | 31 | const valueT = (obj) => { 32 | const duration = moment.duration(`PT${obj.hour}H${obj.minute}M${obj.second}S`); 33 | return duration.asSeconds(); 34 | }; 35 | 36 | const valueInverseT = (value, offset = 'Z') => { 37 | if (value >= 0 && value <= 86400) { 38 | return time(prepareTime(value, offset)); 39 | } 40 | const secondsFromMidnight = value - (Math.floor(value / 86400) * 86400); 41 | const timeStr = prepareTime(secondsFromMidnight, offset); 42 | return time(`${timeStr}`); 43 | }; 44 | 45 | const valueDT = (obj) => { 46 | const e = moment.parseZone(epoch, date_ISO_8601); 47 | const duration = moment.duration(obj.diff(e)); 48 | return duration.asSeconds(); 49 | }; 50 | 51 | const valueInverseDT = (value, offset = 'Z') => { 52 | const e = moment.parseZone(epoch, date_ISO_8601); 53 | return dateAndTime(e.add(value, 'seconds').utcOffset(offset).format()); 54 | }; 55 | 56 | const valueDTD = obj => obj.asSeconds(); 57 | 58 | const valueInverseDTD = value => duration(`PT${Math.floor(value)}S`); 59 | 60 | const valueYMD = obj => obj.asMonths(); 61 | 62 | const valueInverseYMD = value => duration(`P${Math.floor(value)}M`); 63 | 64 | module.exports = { valueT, valueInverseT, valueDT, valueInverseDT, valueDTD, valueInverseDTD, valueYMD, valueInverseYMD }; 65 | -------------------------------------------------------------------------------- /test/function-definition/feel-function-definition.parse.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Function definition grammar test'), function () { 13 | 14 | it('Successfully creates user defined function definition from simple expression', function (done) { 15 | var text = 'function(age) age < 21'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Successfully creates user defined function definition', function (done) { 28 | var text = 'function(rate, term, amount) (amount*rate/12)/(1-(1+rate/12)**-term)'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).not.to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).not.to.be.undefined; 35 | expect(e).to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | 40 | it('Successfully creates external function definition with string key', function (done) { 41 | var text = 'function(angle) external {java: {class : "java.lang.Math", "method signature": "cos(double)"}}'; 42 | 43 | try { 44 | var parsedGrammar = FEEL.parse(text); 45 | expect(parsedGrammar).not.to.be.undefined; 46 | } catch (e) { 47 | expect(parsedGrammar).not.to.be.undefined; 48 | expect(e).to.be.undefined; 49 | } 50 | done(); 51 | }); 52 | 53 | it('Successfully creates external function definition with name key', function (done) { 54 | var text = 'function(angle) external {java: {class : "java.lang.Math", method signature: "cos(double)"}}'; 55 | 56 | try { 57 | var parsedGrammar = FEEL.parse(text); 58 | expect(parsedGrammar).not.to.be.undefined; 59 | } catch (e) { 60 | expect(parsedGrammar).not.to.be.undefined; 61 | expect(e).to.be.undefined; 62 | } 63 | done(); 64 | }); 65 | 66 | }); 67 | -------------------------------------------------------------------------------- /test/context-entry-generator/context-entries-generator-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var XLSX = require('xlsx'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DTable = require('../../utils/helper/decision-logic'); 11 | var fs = require('fs'); 12 | 13 | var excelWorkbookPath = 'test/data/PostBureauRiskCategory2.xlsx'; 14 | 15 | describe('Context generation tests...', function() { 16 | it('should create a FEEL context - case 1', function() { 17 | // string when provided with input containing context entries 18 | var contextEntriesArray = [ 19 | 'Post Bureau Risk Category', 20 | { 21 | "Existing Customer" : "Applicant. ExistingCustomer", 22 | "Credit Score" : "Report. CreditScore" 23 | } 24 | ]; 25 | 26 | var expected = "{Post Bureau Risk Category,Existing Customer : Applicant. ExistingCustomer,Credit Score : Report. CreditScore}" 27 | // debugger; 28 | var contextString = DTable._.generateContextString(contextEntriesArray); 29 | 30 | expect(expected).to.equal(contextString); 31 | }); 32 | 33 | it('should create a FEEL context - case 2', function() { 34 | // string when provided with input containing context entries 35 | var contextEntriesArray = [ 36 | { 37 | "some list": [ 38 | 'value 1', 'value2', 'value3' 39 | ] 40 | } 41 | ]; 42 | 43 | var expected = "{some list : ['value 1','value2','value3']}" 44 | // debugger; 45 | var contextString = DTable._.generateContextString(contextEntriesArray); 46 | 47 | expect(expected).to.equal(contextString); 48 | }); 49 | 50 | it('should create FEEL context string - case 3', function() { 51 | // generateContextString on array with 2nd argument as "csv" 52 | // should give you a simple csv list [val1, val2, val3, ...] 53 | var contextEntries = { 54 | "input expression list": 55 | DTable._.generateContextString( 56 | ["Existing Customer", "Credit Score", "Application Risk Score"], 57 | "csv" 58 | ) 59 | }; 60 | 61 | var computedExpression = DTable._.generateContextString([contextEntries]); 62 | 63 | var expectedExpression = '{input expression list : [Existing Customer,Credit Score,Application Risk Score]}'; 64 | 65 | expect(computedExpression).to.equal(expectedExpression) 66 | }); 67 | 68 | it('should create FEEL context string - case 4', function() { 69 | // generateContextString on array with 2nd argument as "list" 70 | // should give you a simple list - val1, val2, val3, ... 71 | 72 | var list = ["Existing Customer", "Credit Score", "Application Risk Score"]; 73 | var expectedExpression = list.join(','); 74 | 75 | var computedExpression = DTable._.generateContextString(list, "list"); 76 | 77 | expect(computedExpression).to.equal(expectedExpression); 78 | 79 | }); 80 | }) 81 | -------------------------------------------------------------------------------- /grammar/feel-initializer.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | // initializer section start 8 | 9 | // ast nodes are the constructors used to construct the ast for the parsed grammar 10 | const ast = require('./feel-ast'); 11 | // const { enableLexerLogging } = require('../settings'); 12 | 13 | // const {logger} = require('../logger'); 14 | // adding build methods to prototype of each constructor 15 | require('./feel-ast-parser')(ast); 16 | // const _log = logger('feel-grammer-parser'); 17 | // let loggerOptions; 18 | // function log(msg) { 19 | // loggerOptions = options.loggerOptions || {}; 20 | // if (enableLexerLogging) { 21 | // _log.debug(loggerOptions, msg) 22 | // } 23 | // }; 24 | 25 | function log() { 26 | // empty function 27 | } 28 | 29 | let initialized = false; 30 | let ruleName = 'default'; 31 | 32 | function rule() { 33 | if (!initialized) { 34 | ruleName = options.ruleName 35 | initialized = true 36 | } 37 | return ruleName; 38 | } 39 | 40 | function extractOptional(optional, index) { 41 | //log('_extractOptional'); 42 | return optional ? optional[index] : null; 43 | } 44 | 45 | function flatten(list) { 46 | //log('_flatten'); 47 | return list.filter( d => d && d.length).reduce((recur, next) => { 48 | if(next && Array.isArray(next)) { 49 | return [].concat.call(recur, flatten(next)); 50 | } 51 | return [].concat.call(recur, next); 52 | }, []); 53 | } 54 | 55 | function extractList(list, index) { 56 | //log('_extractList'); 57 | return list.map(element => element[index]); 58 | } 59 | 60 | function buildList(head, tail, index) { 61 | //log('_buildList') 62 | return [head].concat(extractList(tail, index)); 63 | } 64 | 65 | function buildName(head, tail, index) { 66 | //log('_buildName'); 67 | return tail && tail.length ? [...head, ...flatten(tail)].join("") : head.join(""); 68 | } 69 | 70 | 71 | function buildBinaryExpression(head, tail, loc, text, rule) { 72 | //log('_buildBinaryExpression'); 73 | return tail.reduce((result, element) => new ast.ArithmeticExpressionNode(element[1], result, element[3], loc, text, rule), head); 74 | } 75 | 76 | function buildComparisionExpression(head, tail, loc, text, rule) { 77 | //log('_buildComparisionExpression'); 78 | return tail.reduce((result, element) => { 79 | const operator = Array.isArray(element[1]) ? element[1][0] : element[1]; 80 | return new ast.ComparisionExpressionNode(operator, result, element[3], null, loc, text, rule); 81 | }, head); 82 | } 83 | 84 | function buildLogicalExpression(head, tail, loc, text, rule) { 85 | //log('_buildLogicalExpression'); 86 | return tail.reduce((result, element) => { 87 | let operator = element[1]; 88 | if (operator === 'and') { 89 | operator = '&&'; 90 | } else if (operator === 'or') { 91 | operator = '||'; 92 | } 93 | return new ast.LogicalExpressionNode(operator, result, element[3], loc, text, rule); 94 | }, head); 95 | } 96 | 97 | -------------------------------------------------------------------------------- /test/external-function/decision-table-external-function-evaluation.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const expect = chai.expect; 10 | const DTable = require('../../utils/helper/decision-table'); 11 | const DTree = require('../../utils/helper/decision-tree'); 12 | const xlArr = ['RiskCategoryEvaluation.xlsx','LoanApplicationValidity.xlsx', 'BillCalculation.xlsx']; 13 | let decision_table = {}; 14 | let csv = {}; 15 | let i = 0; 16 | 17 | describe(chalk.blue('External function definition and evaluation'), () => { 18 | 19 | before('setup test data, read excel file and get the decision table', (done) => { 20 | done(); 21 | }); 22 | 23 | beforeEach('prepare decision table from excel and set the payload', (done) => { 24 | const path = './test/data/' + xlArr[i++]; 25 | csv = DTable.xls_to_csv(path); 26 | decision_table = DTable.csv_to_decision_table(csv[0]); 27 | done(); 28 | }); 29 | 30 | it('External Function - RiskCategoryEvaluation', (done) => { 31 | const payload = {"Applicant": {"ExistingCustomer" : true}, "Report": {"CreditScore" : 600}, "b" : 60}; 32 | DTable.execute_decision_table("RiskCategoryEvaluation", decision_table, payload, (err, results)=> { 33 | if(err){ 34 | return done(err); 35 | } 36 | expect(results.PostBureauRiskCategory).to.equal('MEDIUM'); 37 | done(); 38 | }); 39 | }); 40 | 41 | it('External Function - LoanApplicationValidity', (done) => { 42 | const payload = { 43 | "organisation": "A Corp", 44 | "designation": "SSE", 45 | "state": "KA", 46 | "pincode": "560100", 47 | "loanAmount": 40000, 48 | "basePay": 250000, 49 | "experience": 3, 50 | "age": 55, 51 | "address proof": false 52 | } 53 | DTable.execute_decision_table("LoanApplicationValidity", decision_table, payload, (err, results)=> { 54 | if(err){ 55 | return done(err); 56 | } 57 | expect(results.length).to.equal(2); 58 | expect(results[0].errCode).to.equal('err-employer-check'); 59 | expect(results[0].errMessage).to.equal('employer details(employeeId, department and organisation) were not given'); 60 | expect(results[1].errCode).to.equal('err-address-check'); 61 | expect(results[1].errMessage).to.equal('passport details alongwith city necessary'); 62 | done(); 63 | }); 64 | }); 65 | 66 | it('External Function - BillCalculation', (done) => { 67 | const payload = { "State" : "Karnataka", "Units" : 31 }; 68 | DTable.execute_decision_table("BillCalculation", decision_table, payload, (err, results)=> { 69 | if(err){ 70 | return done(err); 71 | } 72 | expect(results.Amount).to.equal(94.4); 73 | done(); 74 | }); 75 | }); 76 | 77 | }); 78 | -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/date.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | Decision Model and Notation, v1.1 10 | Page : 131 11 | */ 12 | 13 | /* 14 | Format : date(from) // from - date string 15 | Description : convert "from" to a date 16 | e.g. : date("2012-12-25") – date("2012-12-24") = duration("P1D") 17 | */ 18 | 19 | /* 20 | Format : date and time(from) // from - date_and_time 21 | Description : convert "from" to a date (set time components to null) 22 | e.g. : date(date and time("2012-12-25T11:00:00Z")) = date("2012-12-25") 23 | */ 24 | 25 | /* 26 | Format : date(year, month, day) // year, month, day are numbers 27 | Description : creates a date from year, month, day component values 28 | e.g. : date(2012, 12, 25) = date("2012-12-25") 29 | */ 30 | 31 | const moment = require('moment-timezone'); 32 | const addProperties = require('./add-properties'); 33 | const { types, properties, UTC, UTCTimePart, time_ISO_8601, date_ISO_8601 } = require('../../helper/meta'); 34 | 35 | const { year, month, day } = properties; 36 | const props = Object.assign({}, { year, month, day, type: types.date, isDate: true }); 37 | 38 | const isNumber = args => args.reduce((prev, next) => prev && typeof next === 'number', true); 39 | 40 | const parseDate = (str) => { 41 | try { 42 | const d = moment.parseZone(`${str}${UTCTimePart}`); 43 | if (d.isValid()) { 44 | return d; 45 | } 46 | throw new Error('Invalid date. This is usually caused by an inappropriate format. Please check the input format.'); 47 | } catch (err) { 48 | return err; 49 | } 50 | }; 51 | 52 | const date = (...args) => { 53 | let d; 54 | if (args.length === 1) { 55 | const arg = args[0]; 56 | if (typeof arg === 'string') { 57 | try { 58 | d = arg === '' ? moment.parseZone(UTCTimePart, time_ISO_8601) : parseDate(arg); 59 | } catch (err) { 60 | throw err; 61 | } 62 | } else if (typeof arg === 'object') { 63 | if (arg instanceof Date) { 64 | const ISO = arg.toISOString(); 65 | const dateTime = moment.parseZone(ISO); 66 | const datePart = dateTime.format(date_ISO_8601); 67 | d = moment.parseZone(`${datePart}${UTCTimePart}`); 68 | } else if (arg.isDateTime) { 69 | const dateTime = moment.tz(arg.format(), UTC); 70 | const datePart = dateTime.format(date_ISO_8601); 71 | d = moment.parseZone(`${datePart}${UTCTimePart}`); 72 | } 73 | if (!d.isValid()) { 74 | throw new Error('Invalid date. Parsing error while attempting to create date from date and time'); 75 | } 76 | } else { 77 | throw new Error('Invalid format encountered. Please specify date in one of these formats :\n- "date("2012-12-25")"\n- date_and_time object'); 78 | } 79 | } else if (args.length === 3 && isNumber(args)) { 80 | const [year, month, day] = args; 81 | d = moment.tz({ year, month, day, hour: 0, minute: 0, second: 0 }, UTC); 82 | if (!d.isValid()) { 83 | throw new Error('Invalid date. Parsing error while attempting to create date from parts'); 84 | } 85 | } else { 86 | throw new Error('Invalid number of arguments specified with "date" in-built function'); 87 | } 88 | 89 | return addProperties(d, props); 90 | }; 91 | 92 | module.exports = { date }; 93 | 94 | -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/date-time.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | Decision Model and Notation, v1.1 10 | Page : 131 11 | */ 12 | 13 | /* 14 | Format : date_and_time(date, time) 15 | Description : date is a date or date time; time is a time creates a date time from the given date (ignoring any time component) and the given time 16 | e.g. : date_and_time("2012-12-24T23:59:00") = date_and_time(date("2012-12-24”), time(“T23:59:00")) 17 | */ 18 | 19 | /* 20 | Format : date and time(from) // from - date time string 21 | Description : date time string convert "from" to a date and time 22 | e.g. : date and time("2012-12-24T23:59:00") + duration("PT1M") = date and time("2012-12-25T00:00:00") 23 | */ 24 | 25 | const moment = require('moment-timezone'); 26 | const addProperties = require('./add-properties'); 27 | const { time_ISO_8601, date_ISO_8601, date_time_IANA_tz, types, properties } = require('../../helper/meta'); 28 | 29 | const { year, month, day, hour, minute, second, 'time offset': time_offset, timezone } = properties; 30 | const props = Object.assign({}, { year, month, day, hour, minute, second, 'time offset': time_offset, timezone, type: types.date_and_time, isDateTime: true }); 31 | 32 | const parseIANATz = (str) => { 33 | const match = str.match(date_time_IANA_tz); 34 | if (match) { 35 | const [dateTime, timeZone] = match.slice(1); 36 | if (dateTime && timeZone) { 37 | try { 38 | const dt = moment(dateTime).tz(timeZone); 39 | if (dt.isValid()) { 40 | return dt; 41 | } 42 | throw new Error('Invalid date and time in IANA tz format. Please check the input format'); 43 | } catch (err) { 44 | throw err; 45 | } 46 | } 47 | throw new Error(`Error parsing IANA format input. One or more parts are missing. DateTimePart : ${dateTime} TimeZonePart : ${timeZone}`); 48 | } 49 | return match; 50 | }; 51 | 52 | const dateAndTime = (...args) => { 53 | let dt; 54 | if (args.length === 1) { 55 | const arg = args[0]; 56 | const str = arg instanceof Date ? arg.toISOString() : arg; 57 | if (typeof str === 'string') { 58 | try { 59 | dt = str === '' ? moment() : parseIANATz(str) || moment.parseZone(str); 60 | } catch (err) { 61 | throw err; 62 | } 63 | } 64 | if (!dt.isValid()) { 65 | throw new Error('Invalid date_and_time. This is usually caused by an invalid format. Please check the input format'); 66 | } 67 | } else if (args.length === 2) { 68 | const [date, time] = args; 69 | if (date && date.isDate && time && time.isTime) { 70 | const datePart = date.format(date_ISO_8601); 71 | const timePart = time.format(time_ISO_8601); 72 | dt = moment.parseZone(`${datePart}${timePart}`); 73 | if (!dt.isValid()) { 74 | throw new Error('Invalid date and time. This is usually caused by input type mismatch.'); 75 | } 76 | } else { 77 | throw new Error('Type Mismatch - args specified with date_and_time are expected to be of type date and time respectively. Please check the arguments order or type'); 78 | } 79 | } else { 80 | throw new Error('Invalid number of arguments specified with "date_and_time" in-built function'); 81 | } 82 | 83 | return addProperties(dt, props); 84 | }; 85 | 86 | module.exports = { 'date and time': dateAndTime }; 87 | -------------------------------------------------------------------------------- /test/date-time-expression/feel-time.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const FEEL = require('../../dist/feel'); 10 | 11 | const expect = chai.expect; 12 | 13 | describe(chalk.blue('time built-in function grammar test'), function () { 14 | 15 | it('should parse time with format "HH:mm:ssZ"', function (done) { 16 | const text = 'time("13:01:05+05:30").isTime'; 17 | try { 18 | const parsedGrammar = FEEL.parse(text); 19 | parsedGrammar.build() 20 | .then((result) => { 21 | expect(result).to.be.true; 22 | done(); 23 | }).catch(err => { 24 | done(err); 25 | }); 26 | } catch (err) { 27 | done(err); 28 | } 29 | }); 30 | 31 | it('should parse time with format "([0-9]{2}):([0-9]{2}):([0-9]{2})(?:@(.+))+"', function (done) { 32 | const text = 'time("00:01:00@Etc/UTC").isTime'; 33 | try { 34 | const parsedGrammar = FEEL.parse(text); 35 | parsedGrammar.build() 36 | .then((result) => { 37 | expect(result).to.be.true; 38 | done(); 39 | }).catch(err => { 40 | done(err); 41 | }); 42 | } catch (err) { 43 | done(err); 44 | } 45 | }); 46 | 47 | it('should extract hour part from time', function (done) { 48 | const text = 'time("13:10:05@Etc/UTC").hour'; 49 | try { 50 | const parsedGrammar = FEEL.parse(text); 51 | parsedGrammar.build() 52 | .then((result) => { 53 | expect(result).to.equal(13); 54 | done(); 55 | }).catch(err => { 56 | done(err); 57 | }); 58 | } catch (err) { 59 | done(err); 60 | } 61 | }); 62 | 63 | it('should extract minute part from time', function (done) { 64 | const text = 'time("13:10:05@Etc/UTC").minute'; 65 | try { 66 | const parsedGrammar = FEEL.parse(text); 67 | parsedGrammar.build() 68 | .then((result) => { 69 | expect(result).to.equal(10); 70 | done(); 71 | }).catch(err => { 72 | done(err); 73 | }); 74 | } catch (err) { 75 | done(err); 76 | } 77 | }); 78 | 79 | it('should extract second part from time', function (done) { 80 | const text = 'time("13:10:05@Etc/UTC").second'; 81 | try { 82 | const parsedGrammar = FEEL.parse(text); 83 | parsedGrammar.build() 84 | .then((result) => { 85 | expect(result).to.equal(5); 86 | done(); 87 | }).catch(err => { 88 | done(err); 89 | }); 90 | } catch (err) { 91 | done(err); 92 | } 93 | }); 94 | 95 | it('should extract timezone part from time', function (done) { 96 | const text = 'time("13:10:05@Etc/UTC").timezone'; 97 | try { 98 | const parsedGrammar = FEEL.parse(text); 99 | parsedGrammar.build() 100 | .then((result) => { 101 | expect(result).to.equal("Etc/UTC"); 102 | done(); 103 | }).catch(err => { 104 | done(err); 105 | }); 106 | } catch (err) { 107 | done(err); 108 | } 109 | }); 110 | }); 111 | -------------------------------------------------------------------------------- /test/comparision-expression/feel-comparision-expression.parse.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Comparision expression grammar test'), function () { 13 | 14 | it('Successfully creates ast from simple comparision expression', function (done) { 15 | var text = '5 in (<= 5)'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Successfully creates ast from comparision expression', function (done) { 28 | var text = '5 in ((5..10])'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).not.to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).not.to.be.undefined; 35 | expect(e).to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | 40 | it('Successfully creates ast from comparision expression', function (done) { 41 | var text = '5 in ([5..10])'; 42 | 43 | try { 44 | var parsedGrammar = FEEL.parse(text); 45 | expect(parsedGrammar).not.to.be.undefined; 46 | } catch (e) { 47 | expect(parsedGrammar).not.to.be.undefined; 48 | expect(e).to.be.undefined; 49 | } 50 | done(); 51 | }); 52 | 53 | it('Successfully creates ast from comparision expression', function (done) { 54 | var text = '5 in (4,5,6)'; 55 | 56 | try { 57 | var parsedGrammar = FEEL.parse(text); 58 | expect(parsedGrammar).not.to.be.undefined; 59 | } catch (e) { 60 | expect(parsedGrammar).not.to.be.undefined; 61 | expect(e).to.be.undefined; 62 | } 63 | done(); 64 | }); 65 | 66 | it('Successfully creates ast from comparision expression', function (done) { 67 | var text = '5 in (<5,>5)'; 68 | 69 | try { 70 | var parsedGrammar = FEEL.parse(text); 71 | expect(parsedGrammar).not.to.be.undefined; 72 | } catch (e) { 73 | expect(parsedGrammar).not.to.be.undefined; 74 | expect(e).to.be.undefined; 75 | } 76 | done(); 77 | }); 78 | 79 | it('Successfully creates ast from comparision expression', function (done) { 80 | var text = '(a + 5) >= (7 + g)'; 81 | 82 | try { 83 | var parsedGrammar = FEEL.parse(text); 84 | expect(parsedGrammar).not.to.be.undefined; 85 | } catch (e) { 86 | expect(parsedGrammar).not.to.be.undefined; 87 | expect(e).to.be.undefined; 88 | } 89 | done(); 90 | }); 91 | 92 | it('Successfully creates ast from comparision expression', function (done) { 93 | var text = '(a+b) between (c + d) and (e - f)'; 94 | 95 | try { 96 | var parsedGrammar = FEEL.parse(text); 97 | expect(parsedGrammar).not.to.be.undefined; 98 | } catch (e) { 99 | expect(parsedGrammar).not.to.be.undefined; 100 | expect(e).to.be.undefined; 101 | } 102 | done(); 103 | }); 104 | }); -------------------------------------------------------------------------------- /test/data/RoutingDecisionService.json: -------------------------------------------------------------------------------- 1 | { 2 | "Routing": "Routing Rules (Bankrupt : Bureau data . Bankrupt,Credit Score : Bureau data . CreditScore,Post Bureau Risk Category : Post bureau risk category,Post Bureau Affordability : Post bureau affordability)", 3 | "Routing Rules": "decision table(outputs : \"Routing\",input expression list : ['Post Bureau Risk Category','Post Bureau Affordability','Bankrupt','Credit Score'],rule list : [['-','FALSE','-','-','\"DECLINE\"'],['-','-','TRUE','-','\"DECLINE\"'],['\"HIGH\"','-','-','-','\"REFER\"'],['-','-','-','<580','\"REFER\"'],['-','-','-','-','\"ACCEPT\"']],id : 'Routing Rules',hit policy: \"P\",input values list : [[],[],[],['null','[0..999]']],output values : [['\"DECLINE\"','\"REFER\"','\"ACCEPT\"']])", 4 | "Post bureau risk category": "Post Bureau risk category table (Existing Customer : Applicant data . ExistingCustomer,Credit Score : Bureau data . CreditScore,Application Risk Score : Application risk score)", 5 | "Post Bureau risk category table": "decision table(outputs : \"Post Bureau Risk Category\",input expression list : ['Existing Customer','Application Risk Score','Credit Score'],rule list : [['FALSE','< 120','<590','\"HIGH\"'],['FALSE','< 120','[590..610]','\"MEDIUM\"'],['FALSE','< 120','> 610','\"LOW\"'],['FALSE','[120..130]','<600','\"HIGH\"'],['FALSE','[120..130]','[600..625]','\"MEDIUM\"'],['FALSE','[120..130]','> 625','\"LOW\"'],['FALSE','> 130','-','\"VERY LOW\"'],['TRUE','<= 100','< 580','\"HIGH\"'],['TRUE','<= 100','[580..600]','\"MEDIUM\"'],['TRUE','<= 100','> 600','\"LOW\"'],['TRUE','> 100','< 590','\"HIGH\"'],['TRUE','> 100','[590..615]','\"MEDIUM\"'],['TRUE','> 100','> 615','\"LOW\"']],id : 'Post Bureau risk category table',hit policy: \"U\")", 6 | "Post bureau affordability": "Affordability calculation (Monthly Income : Applicant data . Monthly . Income,Monthly Repayments : Applicant data . Monthly . Repayments,Monthly Expenses : Applicant data . Monthly . Expenses,Risk Category : Post bureau risk category,Required Monthly Installment : Required monthly installment)", 7 | "Affordability calculation": "{Disposable Income : Monthly Income - (Monthly Repayments + Monthly Expenses),Credit Contingency Factor : Credit contingency factor,Affordability : if Disposable Income * Credit Contingency Factor > Required Monthly Installment then true else false,result : Affordability}", 8 | "Credit contingency factor": "Credit Contingency factor table (Risk Category : Risk Category)", 9 | "Credit Contingency factor table": "decision table(outputs : \"Credit Contingency Factor\",input expression list : ['Risk Category'],rule list : [['\"HIGH\",\"DECLINE\"','0.6'],['\"MEDIUM\"','0.7'],['\"LOW\",\"VERY LOW\"','0.8']],id : 'Credit Contingency factor table',hit policy: \"U\")", 10 | "Required monthly installment": "Installment Calculation (Product Type : Requested product . ProductType,Rate : Requested product . Rate,Term : Requested product . Term,Amount : Requested product . Amount)", 11 | "Installment Calculation": "{Monthly Fee : if Product Type = \"STANDARD LOAN\" then 20.00 else if Product Type = \"SPECIAL LOAN\" then 25.00 else null,Monthly Repayment : PMT(Rate, Term, Amount),result : Monthly Repayment + Monthly Fee}", 12 | "Application risk score": "Application risk score model (Age : Applicant data . Age,Marital Status : Applicant data . MaritalStatus,Employment Status : Applicant data . EmploymentStatus)", 13 | "Application risk score model": "decision table(outputs : \"Partial Score\",input expression list : ['Age','Marital Status','Employment Status'],rule list : [['[18..21]','-','-','32'],['[22..25]','-','-','35'],['[26..35]','-','-','40'],['[36..49]','-','-','43'],['>=50','-','-','48'],['-','\"S\"','-','25'],['-','\"M\"','-','45'],['-','-','\"UNEMPLOYED\"','15'],['-','-','\"STUDENT\"','18'],['-','-','\"EMPLOYED\"','45'],['-','-','\"SELF-EMPLOYED\"','36']],id : 'Application risk score model',hit policy: \"C+\",input values list : [[18..120],['\"S\"','\"M\"'],['\"UNEMPLOYED\"','\"EMPLOYED\"','\"SELF-EMPLOYED\"','\"STUDENT\"']],output values : [[]])" 14 | } -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/duration.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | Decision Model and Notation, v1.1 10 | Page : 131 11 | */ 12 | 13 | /* 14 | Format : duration(from) // from - duration string 15 | Description :convert "from" to a days and time or years and months duration 16 | e.g. : date_and_time("2012-12-24T23:59:00") - date_and_time("2012-12-22T03:45:00") = duration("P2DT20H14M") 17 | duration("P2Y2M") = duration("P26M") 18 | */ 19 | 20 | /* 21 | Format : years_and_months_duration(from, to) // both are date_and_time 22 | Description : return years and months duration between "from" and "to" 23 | e.g. : years and months duration(date("2011-12-22"), date("2013-08-24")) = duration("P1Y8M") 24 | */ 25 | 26 | const moment = require('moment'); 27 | const addProperties = require('./add-properties'); 28 | const { ymd_ISO_8601, dtd_ISO_8601, types, properties } = require('../../helper/meta'); 29 | 30 | const { years, months, days, hours, minutes, seconds } = properties; 31 | const dtdProps = Object.assign({}, { days, hours, minutes, seconds, type: types.dtd, isDtd: true, isDuration: true }); 32 | const ymdProps = Object.assign({}, { years, months, type: types.ymd, isYmd: true, isDuration: true }); 33 | 34 | const isDateTime = args => args.reduce((recur, next) => recur && (next.isDateTime || next.isDate), true); 35 | 36 | const daysAndTimeDuration = (...args) => { 37 | let dtd; 38 | if (args.length === 1) { 39 | dtd = moment.duration(args[0]); 40 | dtd = dtd.isValid() ? dtd : new Error('Invalid Duration : "days_and_time_duration" in-built function'); 41 | } else if (args.length === 2 && isDateTime(args)) { 42 | const [start, end] = args; 43 | dtd = moment.duration(Math.floor(end.diff(start))); 44 | } else { 45 | throw new Error('Invalid number of arguments specified with "days_and_time_duration" in-built function'); 46 | } 47 | 48 | if (dtd instanceof Error) { 49 | throw dtd; 50 | } else { 51 | return addProperties(dtd, dtdProps); 52 | } 53 | }; 54 | 55 | const yearsAndMonthsDuration = (...args) => { 56 | let ymd; 57 | if (args.length === 1) { 58 | ymd = moment.duration(args[0]); 59 | ymd = ymd.isValid() ? ymd : new Error('Invalid Duration : "years_and_months_duration" in-built function'); 60 | } else if (args.length === 2 && isDateTime(args)) { 61 | const [start, end] = args; 62 | const months = Math.floor(moment.duration(end.diff(start)).asMonths()); 63 | ymd = moment.duration(months, 'months'); 64 | } else { 65 | throw new Error('Invalid number of arguments specified with "years_and_months_duration" in-built function'); 66 | } 67 | 68 | if (ymd instanceof Error) { 69 | throw ymd; 70 | } else { 71 | return addProperties(ymd, ymdProps); 72 | } 73 | }; 74 | 75 | // slice(1) is necessary as "P" will be available in both the patterns and we need to check if some optional part is not undefined to determine a type 76 | const patternMatch = (arg, pattern) => arg.match(pattern).slice(1).reduce((recur, next) => recur || Boolean(next), false); 77 | 78 | const duration = (arg) => { 79 | if (typeof arg === 'string') { 80 | if (patternMatch(arg, ymd_ISO_8601)) { 81 | try { 82 | return yearsAndMonthsDuration(arg); 83 | } catch (err) { 84 | throw err; 85 | } 86 | } else if (patternMatch(arg, dtd_ISO_8601)) { 87 | try { 88 | return daysAndTimeDuration(arg); 89 | } catch (err) { 90 | throw err; 91 | } 92 | } 93 | throw new Error('Invalid Format : "duration" built-in function. Please check the input format'); 94 | } 95 | throw new Error(`Type Error : "duration" built-in function expects a string but "${typeof arg}" encountered`); 96 | }; 97 | 98 | 99 | module.exports = { duration, 'years and months duration': yearsAndMonthsDuration, 'days and time duration': daysAndTimeDuration }; 100 | -------------------------------------------------------------------------------- /test/decision-service/feel-invocation-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var XLSX = require('xlsx'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DL = require('../../utils/helper/decision-logic'); 11 | var fs = require('fs'); 12 | 13 | var testDataFile = 'test/data/RoutingDecisionService.xlsx'; 14 | var testDataFile2 = 'test/data/ApplicantData.xlsx'; 15 | 16 | describe('boxed expression tests...', function() { 17 | it('should generate the required boxed invocation expression from worksheet', function() { 18 | var workbook = XLSX.readFile(testDataFile); 19 | 20 | var worksheet = workbook.Sheets["Application Risk Score"]; 21 | 22 | var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 23 | // debugger; 24 | var isBoxed = DL._.isBoxedInvocation(csvExcel); 25 | 26 | expect(isBoxed).to.equal(true); 27 | 28 | //defining below the context string for this sheet 29 | var generateContextString = DL._.generateContextString; 30 | 31 | var funcContext = { 32 | "Age" : "Applicant data . Age", 33 | "Marital Status" : "Applicant data . MaritalStatus", 34 | "Employment Status" : "Applicant data . EmploymentStatus" 35 | }; 36 | 37 | var funcContextString = generateContextString(funcContext, "csv"); 38 | 39 | var fnName = "Application risk score model"; 40 | 41 | var expectedCtxString = `${fnName} (${funcContextString})`; 42 | 43 | var result = DL._.makeContext(csvExcel); 44 | 45 | expect(result.expression).to.equal(expectedCtxString); 46 | }); 47 | 48 | 49 | it('should generate expression for boxed context with result correctly', function() { 50 | var generateContextString = DL._.generateContextString; 51 | var workbook = XLSX.readFile(testDataFile); 52 | 53 | var worksheet = workbook.Sheets["Installment Calculation"]; 54 | 55 | expect(worksheet).to.be.defined; 56 | 57 | var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 58 | 59 | var isContextWithResult = DL._.isBoxedContextWithResult(csvExcel); 60 | 61 | expect(isContextWithResult).to.be.true; 62 | 63 | var contextEntries = { 64 | "Monthly Fee" : "if Product Type = \"STANDARD LOAN\" " 65 | + "then 20.00 " 66 | + "else if Product Type = \"SPECIAL LOAN\" " 67 | + "then 25.00 " 68 | + "else null", 69 | "Monthly Repayment" : "PMT(Rate, Term, Amount)", 70 | "result" : "Monthly Repayment + Monthly Fee" 71 | }; 72 | 73 | var expectedCtxString = generateContextString(contextEntries, false); 74 | 75 | var computedCtxString = DL._.makeContext(csvExcel).expression; 76 | // fs.writeFileSync('file1.txt', computedCtxString) 77 | // fs.writeFileSync('file2.txt', expectedCtxString) 78 | expect(computedCtxString).to.equal(expectedCtxString); 79 | 80 | }); 81 | 82 | it('should generate expression for boxed context without result', function() { 83 | var generateContextString = DL._.generateContextString; 84 | var workbook = XLSX.readFile(testDataFile2); 85 | 86 | var worksheet = workbook.Sheets["Applicant Data"]; 87 | 88 | expect(worksheet).to.be.defined; 89 | 90 | var csvExcel = XLSX.utils.sheet_to_csv(worksheet, { FS: '&SP', RS: '&RSP'}); 91 | expect(csvExcel.length).to.not.equal(0) 92 | var isContext = DL._.isBoxedContextWithoutResult(csvExcel); 93 | expect(isContext).to.be.true; 94 | var contextEntries = { 95 | "Age" : '51', 96 | "MaritalStatus" : '"M"', 97 | EmploymentStatus: '"EMPLOYED"', 98 | ExistingCustomer: 'FALSE' 99 | }; 100 | 101 | var expectedCtxString = generateContextString(contextEntries, false); 102 | // debugger; 103 | var computedCtxString = DL._.makeContext(csvExcel).expression; 104 | 105 | expect(computedCtxString).to.equal(expectedCtxString); 106 | }); 107 | }); 108 | 109 | -------------------------------------------------------------------------------- /utils/built-in-functions/list-functions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const _ = require('lodash'); 9 | 10 | const listContains = (list, element) => { 11 | if (!Array.isArray(list)) { 12 | throw new Error('operation unsupported on element of this type'); 13 | } else { 14 | if (list.indexOf(element) > -1) { return true; } 15 | return false; 16 | } 17 | }; 18 | 19 | const count = (list) => { 20 | if (!Array.isArray(list)) { throw new Error('operation unsupported on element of this type'); } else { 21 | return list.length; 22 | } 23 | }; 24 | 25 | const min = (list) => { 26 | if (!Array.isArray(list)) { 27 | throw new Error('operation unsupported on element of this type'); 28 | } else { 29 | return _.min(list); 30 | } 31 | }; 32 | 33 | const max = (list) => { 34 | if (!Array.isArray(list)) { 35 | throw new Error('operation unsupported on element of this type'); 36 | } else { 37 | return _.max(list); 38 | } 39 | }; 40 | 41 | const sum = (list) => { 42 | if (!Array.isArray(list)) { 43 | throw new Error('operation unsupported on element of this type'); 44 | } else { 45 | return _.sum(list); 46 | } 47 | }; 48 | 49 | const mean = (list) => { 50 | if (!Array.isArray(list)) { 51 | throw new Error('operation unsupported on element of this type'); 52 | } else { 53 | return (_.sum(list)) / (list.length); 54 | } 55 | }; 56 | 57 | const and = (list) => { 58 | if (!Array.isArray(list)) { 59 | throw new Error('operation unsupported on element of this type'); 60 | } else { 61 | return list.reduce((recur, next) => recur && next, true); 62 | } 63 | }; 64 | 65 | const or = (list) => { 66 | if (!Array.isArray(list)) { 67 | throw new Error('operation unsupported on element of this type'); 68 | } else { 69 | return list.reduce((recur, next) => recur || next, false); 70 | } 71 | }; 72 | 73 | const append = (element, list) => { 74 | if (!Array.isArray(list)) { 75 | throw new Error('operation unsupported on element of this type'); 76 | } else { 77 | return list.push(element); 78 | } 79 | }; 80 | 81 | const concatenate = (...args) => args.reduce((result, next) => Array.prototype.concat(result, next), []); 82 | 83 | const insertBefore = (list, position, newItem) => { 84 | if (!Array.isArray(list)) { 85 | throw new Error('operation unsupported on element of this type'); 86 | } else if (position > list.length || position < 0) { 87 | throw new Error('invalid position'); 88 | } else { 89 | return list.splice(position - 1, 0, newItem); 90 | } 91 | }; 92 | 93 | const remove = (list, position) => { 94 | if (!Array.isArray(list)) { 95 | throw new Error('operation unsupported on element of this type'); 96 | } else if (position > list.length - 1) { 97 | throw new Error('invalid position'); 98 | } else { 99 | return list.splice(position, 1); 100 | } 101 | }; 102 | 103 | const reverse = (list) => { 104 | if (!Array.isArray(list)) { 105 | throw new Error('operation unsupported on element of this type'); 106 | } else { 107 | return _.reverse(list); 108 | } 109 | }; 110 | 111 | const indexOf = (list, match) => { 112 | if (!Array.isArray(list)) { 113 | throw new Error('operation unsupported on element of this type'); 114 | } else { 115 | return _.indexOf(list, match); 116 | } 117 | }; 118 | 119 | const union = (...args) => _.union(args); 120 | 121 | const distinctValues = (list) => { 122 | if (!Array.isArray(list)) { 123 | throw new Error('operation unsupported on element of this type'); 124 | } else { 125 | return _.uniq(list); 126 | } 127 | }; 128 | 129 | const flatten = (...args) => _.flatten(args); 130 | 131 | module.exports = { 132 | 'list contains': listContains, 133 | count, 134 | min, 135 | max, 136 | sum, 137 | mean, 138 | and, 139 | or, 140 | append, 141 | concatenate, 142 | 'insert before': insertBefore, 143 | remove, 144 | reverse, 145 | 'index of': indexOf, 146 | union, 147 | 'distinct values': distinctValues, 148 | flatten, 149 | }; 150 | -------------------------------------------------------------------------------- /test/disjunction-conjunction-expression/feel-disjunction-conjunction.parse.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Disjunction-Conjunction grammar test'), function() { 13 | 14 | it('Successfully creates ast from simple disjunction expression', function(done) { 15 | var text = 'a or b'; 16 | 17 | try { 18 | var parsedGrammar = FEEL.parse(text); 19 | expect(parsedGrammar).not.to.be.undefined; 20 | } catch (e) { 21 | expect(parsedGrammar).not.to.be.undefined; 22 | expect(e).to.be.undefined; 23 | } 24 | done(); 25 | }); 26 | 27 | it('Successfully creates ast from simple conjunction expression', function(done) { 28 | var text = 'a and b'; 29 | 30 | try { 31 | var parsedGrammar = FEEL.parse(text); 32 | expect(parsedGrammar).not.to.be.undefined; 33 | } catch (e) { 34 | expect(parsedGrammar).not.to.be.undefined; 35 | expect(e).to.be.undefined; 36 | } 37 | done(); 38 | }); 39 | 40 | it('Successfully creates ast from given logical expression 1', function(done) { 41 | var text = '((a or b) and (b or c)) or (a and d)'; 42 | 43 | try { 44 | var parsedGrammar = FEEL.parse(text); 45 | expect(parsedGrammar).not.to.be.undefined; 46 | } catch (e) { 47 | expect(parsedGrammar).not.to.be.undefined; 48 | expect(e).to.be.undefined; 49 | } 50 | done(); 51 | }); 52 | 53 | it('Successfully creates ast from given logical expression 2', function(done) { 54 | var text = '((a > b) and (a > c)) and (b > c)'; 55 | 56 | try { 57 | var parsedGrammar = FEEL.parse(text); 58 | expect(parsedGrammar).not.to.be.undefined; 59 | } catch (e) { 60 | expect(parsedGrammar).not.to.be.undefined; 61 | expect(e).to.be.undefined; 62 | } 63 | done(); 64 | }); 65 | 66 | it('Successfully creates ast from given logical expression 3', function(done) { 67 | var text = '((a + b) > (c - d)) and (a > b)'; 68 | 69 | try { 70 | var parsedGrammar = FEEL.parse(text); 71 | expect(parsedGrammar).not.to.be.undefined; 72 | } catch (e) { 73 | expect(parsedGrammar).not.to.be.undefined; 74 | expect(e).to.be.undefined; 75 | } 76 | done(); 77 | }); 78 | 79 | it('Successfully creates ast from given logical expression 4', function(done) { 80 | var text = 'a or b or a > b'; 81 | 82 | try { 83 | var parsedGrammar = FEEL.parse(text); 84 | expect(parsedGrammar).not.to.be.undefined; 85 | } catch (e) { 86 | expect(parsedGrammar).not.to.be.undefined; 87 | expect(e).to.be.undefined; 88 | } 89 | done(); 90 | }); 91 | 92 | it('Successfully creates ast from given logical expression 5', function(done) { 93 | var text = '(x(i, j) = "somevalue") and (a > b)'; 94 | 95 | try { 96 | var parsedGrammar = FEEL.parse(text); 97 | expect(parsedGrammar).not.to.be.undefined; 98 | } catch (e) { 99 | expect(parsedGrammar).not.to.be.undefined; 100 | expect(e).to.be.undefined; 101 | } 102 | done(); 103 | }); 104 | 105 | it('Successfully creates ast from given logical expression 6', function(done) { 106 | var text = '(a + b) > (c - d) and (a > b)'; 107 | 108 | try { 109 | var parsedGrammar = FEEL.parse(text); 110 | expect(parsedGrammar).not.to.be.undefined; 111 | expect(parsedGrammar.body.type).to.equal("LogicalExpression"); 112 | } catch (e) { 113 | expect(parsedGrammar).not.to.be.undefined; 114 | expect(e).to.be.undefined; 115 | } 116 | done(); 117 | }); 118 | 119 | }); -------------------------------------------------------------------------------- /test/date-time-expression/feel-duration.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const FEEL = require('../../dist/feel'); 10 | 11 | const expect = chai.expect; 12 | debugger; 13 | describe(chalk.blue('duration built-in function grammar test'), () => { 14 | it('should parse days and time duration', (done) => { 15 | const text = 'duration("P2DT20H14M").isDtd'; 16 | try { 17 | const parsedGrammar = FEEL.parse(text); 18 | parsedGrammar.build() 19 | .then((result) => { 20 | expect(result).to.be.true; 21 | done(); 22 | }).catch((err) => { 23 | done(err); 24 | }); 25 | } catch (err) { 26 | done(err); 27 | } 28 | }); 29 | 30 | it('should parse years and months duration', (done) => { 31 | const text = 'duration("P1Y2M").isYmd'; 32 | try { 33 | const parsedGrammar = FEEL.parse(text); 34 | parsedGrammar.build() 35 | .then((result) => { 36 | expect(result).to.be.true; 37 | done(); 38 | }).catch((err) => { 39 | done(err); 40 | }); 41 | } catch (err) { 42 | done(err); 43 | } 44 | }); 45 | 46 | it('should normalize "P13M" years and months duration to 1 year and 1 month and extract the years part', (done) => { 47 | const text = 'duration("P13M").years'; 48 | try { 49 | const parsedGrammar = FEEL.parse(text); 50 | parsedGrammar.build() 51 | .then((result) => { 52 | expect(result).to.equal(1); 53 | done(); 54 | }).catch((err) => { 55 | done(err); 56 | }); 57 | } catch (err) { 58 | done(err); 59 | } 60 | }); 61 | 62 | it('should extract months part from years and months duration', (done) => { 63 | const text = 'duration("P1Y11M").months'; 64 | try { 65 | const parsedGrammar = FEEL.parse(text); 66 | parsedGrammar.build() 67 | .then((result) => { 68 | expect(result).to.equal(11); 69 | done(); 70 | }).catch((err) => { 71 | done(err); 72 | }); 73 | } catch (err) { 74 | done(err); 75 | } 76 | }); 77 | 78 | it('should extract days part from days and time duration', (done) => { 79 | const text = 'duration("P5DT12H10M").days'; 80 | try { 81 | const parsedGrammar = FEEL.parse(text); 82 | parsedGrammar.build() 83 | .then((result) => { 84 | expect(result).to.equal(5); 85 | done(); 86 | }).catch((err) => { 87 | done(err); 88 | }); 89 | } catch (err) { 90 | done(err); 91 | } 92 | }); 93 | 94 | it('should extract hours part from days and time duration', (done) => { 95 | const text = 'duration("P5DT12H10M").hours'; 96 | try { 97 | const parsedGrammar = FEEL.parse(text); 98 | parsedGrammar.build() 99 | .then((result) => { 100 | expect(result).to.equal(12); 101 | done(); 102 | }).catch((err) => { 103 | done(err); 104 | }); 105 | } catch (err) { 106 | done(err); 107 | } 108 | }); 109 | 110 | it('should extract minutes part from days and time duration', (done) => { 111 | const text = 'duration("P5DT12H10M").minutes'; 112 | try { 113 | const parsedGrammar = FEEL.parse(text); 114 | parsedGrammar.build() 115 | .then((result) => { 116 | expect(result).to.equal(10); 117 | done(); 118 | }).catch((err) => { 119 | done(err); 120 | }); 121 | } catch (err) { 122 | done(err); 123 | } 124 | }); 125 | 126 | it('should extract seconds part from days and time duration', (done) => { 127 | const text = 'duration("P5DT12H10M25S").seconds'; 128 | try { 129 | const parsedGrammar = FEEL.parse(text); 130 | parsedGrammar.build() 131 | .then((result) => { 132 | expect(result).to.equal(25); 133 | done(); 134 | }).catch((err) => { 135 | done(err); 136 | }); 137 | } catch (err) { 138 | done(err); 139 | } 140 | }); 141 | 142 | }); 143 | -------------------------------------------------------------------------------- /test/filter-path-expression/filter-path-expression.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Filter Expression Evaluation'), function() { 13 | it('Successfully builds filter expression to find item at a specific index in an array', function(done) { 14 | var text = 'a[1]'; 15 | 16 | const _context = { 17 | a: [1,2,3,4] 18 | }; 19 | 20 | var parsedGrammar = FEEL.parse(text); 21 | parsedGrammar.build(_context).then(result => { 22 | expect(result).to.equal(2); 23 | done(); 24 | }).catch(err => done(err)); 25 | }); 26 | 27 | it('Successfully builds filter expression to get the sub-array from an array of values', function(done) { 28 | var text = 'a[item > 2]'; 29 | 30 | const _context = { 31 | a: [1,2,3,4] 32 | }; 33 | 34 | var parsedGrammar = FEEL.parse(text); 35 | parsedGrammar.build(_context).then(result => { 36 | expect(result).to.eql([3,4]); 37 | done(); 38 | }).catch(err => done(err)); 39 | }); 40 | 41 | it('Successfully builds filter expression to get the sub-array from an array of objects', function(done) { 42 | var text = 'a[salary > 20000]'; 43 | 44 | const _context = { 45 | a: [{name: 'Foo', salary: 30000}, {name: 'Bar', salary: 21000}, {name: 'Baz', salary: 20000}] 46 | }; 47 | 48 | var parsedGrammar = FEEL.parse(text); 49 | parsedGrammar.build(_context).then(result => { 50 | expect(result).to.eql([{name: 'Foo', salary: 30000}, {name: 'Bar', salary: 21000}]); 51 | done(); 52 | }).catch(err => done(err)); 53 | }); 54 | 55 | it('Successfully builds filter expression with path expression to get an array of values', function(done) { 56 | var text = 'a[salary > 20000].name'; 57 | 58 | const _context = { 59 | a: [{name: 'Foo', salary: 30000}, {name: 'Bar', salary: 21000}, {name: 'Baz', salary: 20000}] 60 | }; 61 | 62 | var parsedGrammar = FEEL.parse(text); 63 | parsedGrammar.build(_context).then(result => { 64 | expect(result).to.eql(['Foo', 'Bar']); 65 | done(); 66 | }).catch(err => done(err)); 67 | }); 68 | 69 | it('Successfully builds chain of filter expressions to get value', function(done) { 70 | const payload= { 71 | a: [{name: 'Foo', salary: 30000}, {name: 'Bar', salary: 21000}, {name: 'Baz', salary: 20000}] 72 | }; 73 | 74 | const context = '{namesWithSalaryGreaterThan20000 : a[salary > 20000].name}' 75 | const text = 'namesWithSalaryGreaterThan20000[1]'; 76 | 77 | const parsedText = FEEL.parse(text); 78 | const parsedContext = FEEL.parse(context); 79 | 80 | parsedContext.build(payload).then(ctx => { 81 | return Object.assign({}, ctx, payload); 82 | }).then((context) => { 83 | return parsedText.build(context); 84 | }).then((result) => { 85 | expect(result).to.equal('Bar'); 86 | done(); 87 | }).catch(err => { 88 | done(err); 89 | }); 90 | }); 91 | 92 | it('Successfully builds chain of path expression and filter expression to get an array and find the sum', function(done) { 93 | debugger; 94 | const payload= {a : { 95 | b: [{name: 'Foo', salary: 30000}, {name: 'Bar', salary: 21000}, {name: 'Baz', salary: 20000}] 96 | }}; 97 | 98 | const context = '{namesWithSalaryGreaterThan20000 : a.b[salary > 20000].name}' 99 | const text = 'sum(namesWithSalaryGreaterThan20000)'; 100 | 101 | const parsedText = FEEL.parse(text); 102 | const parsedContext = FEEL.parse(context); 103 | 104 | parsedContext.build(payload).then(ctx => { 105 | return Object.assign({}, ctx, payload); 106 | }).then((context) => { 107 | return parsedText.build(context); 108 | }).then((result) => { 109 | expect(result).to.equal('FooBar'); 110 | done(); 111 | }).catch(err => { 112 | done(err); 113 | }); 114 | }); 115 | 116 | }); 117 | 118 | // sum(a.b[c > d].e.f[g=h].i) 119 | -------------------------------------------------------------------------------- /test/disjunction-conjunction-expression/feel-disjunction-conjunction.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Disjunction-Conjunction ast parsing test'), function() { 13 | 14 | it('Successfully builds ast from simple disjunction expression', function(done) { 15 | var text = 'a or b'; 16 | 17 | const _context = { 18 | a: true, 19 | b: false 20 | } 21 | 22 | var parsedGrammar = FEEL.parse(text); 23 | parsedGrammar.build(_context).then(result => { 24 | expect(result).not.to.be.undefined; 25 | done(); 26 | }).catch(err => done(err)); 27 | 28 | }); 29 | 30 | it('Successfully builds ast from simple conjunction expression', function(done) { 31 | var text = 'a and b'; 32 | 33 | const _context = { 34 | a: true, 35 | b: false 36 | } 37 | 38 | var parsedGrammar = FEEL.parse(text); 39 | parsedGrammar.build(_context).then(result => { 40 | expect(result).not.to.be.undefined; 41 | done(); 42 | }).catch(err => done(err)); 43 | }); 44 | 45 | it('Successfully builds ast from given logical expression 1', function(done) { 46 | var text = '((a or b) and (b or c)) or (a and d)'; 47 | 48 | const _context = { 49 | a: true, 50 | b: false, 51 | c: true, 52 | d: false 53 | } 54 | 55 | var parsedGrammar = FEEL.parse(text); 56 | parsedGrammar.build(_context).then(result => { 57 | expect(result).not.to.be.undefined; 58 | done(); 59 | }).catch(err => done(err)); 60 | }); 61 | 62 | it('Successfully builds ast from given logical expression 2', function(done) { 63 | var text = '((a > b) and (a > c)) and (b > c)'; 64 | 65 | const _context = { 66 | a: 10, 67 | b: 5, 68 | c: 3 69 | } 70 | 71 | var parsedGrammar = FEEL.parse(text); 72 | parsedGrammar.build(_context).then(result => { 73 | expect(result).not.to.be.undefined; 74 | done(); 75 | }).catch(err => done(err)); 76 | }); 77 | 78 | it('Successfully builds ast from given logical expression 3', function(done) { 79 | var text = '((a + b) > (c - d)) and (a > b)'; 80 | 81 | const _context = { 82 | a: 10, 83 | b: 5, 84 | c: 20, 85 | d: 10 86 | } 87 | 88 | var parsedGrammar = FEEL.parse(text); 89 | parsedGrammar.build(_context).then(result => { 90 | expect(result).not.to.be.undefined; 91 | done(); 92 | }).catch(err => done(err)); 93 | }); 94 | 95 | it('Successfully builds ast from given logical expression 4', function(done) { 96 | var text = 'a or b or a > b'; 97 | 98 | const _context = { 99 | a: 10, 100 | b: 5 101 | } 102 | 103 | var parsedGrammar = FEEL.parse(text); 104 | parsedGrammar.build(_context).then(result => { 105 | expect(result).not.to.be.undefined; 106 | done(); 107 | }).catch(err => done(err)); 108 | }); 109 | 110 | it('Successfully builds ast from given logical expression 5', function(done) { 111 | 112 | var _context_text = '{ x : function(x,y) x+y , y:5 , a:10 , b:5 , i:4 , j:1 }'; 113 | var _text = '(x(i, j) = y) and (a > b)'; 114 | 115 | var parsedContext = FEEL.parse(_context_text); 116 | var parsedGrammar = FEEL.parse(_text); 117 | 118 | 119 | parsedContext.build().then(context => { 120 | parsedGrammar.build(context).then(result => { 121 | expect(result).not.to.be.undefined; 122 | done(); 123 | }).catch(err => done(err)); 124 | }).catch(err => done(err)); 125 | }); 126 | 127 | it('Successfully builds ast from given logical expression 6', function(done) { 128 | var text = '(a + b) > (c - d) and (a > b)'; 129 | 130 | const _context = { 131 | a: 10, 132 | b: 5, 133 | c: 20, 134 | d: 10 135 | } 136 | 137 | var parsedGrammar = FEEL.parse(text); 138 | parsedGrammar.build(_context).then(result => { 139 | expect(result).not.to.be.undefined; 140 | done(); 141 | }).catch(err => done(err)); 142 | }); 143 | }); -------------------------------------------------------------------------------- /test/function-definition/feel-function-definition.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | �2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), Bangalore, India. All Rights Reserved. 3 | The EdgeVerve proprietary software program ("Program"), is protected by copyrights laws, international treaties and other pending or existing intellectual property rights in India, the United States and other countries. 4 | The Program may contain/reference third party or open source components, the rights to which continue to remain with the applicable third party licensors or the open source community as the case may be and nothing here transfers the rights to the third party and open source components, except as expressly permitted. 5 | Any unauthorized reproduction, storage, transmission in any form or by any means (including without limitation to electronic, mechanical, printing, photocopying, recording or otherwise), or any distribution of this Program, or any portion of it, may result in severe civil and criminal penalties, and will be prosecuted to the maximum extent possible under the law. 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var FEEL = require('../../dist/feel'); 11 | 12 | describe(chalk.blue('Function definition grammar test'), function() { 13 | 14 | it('Successfully creates user defined function definition from simple expression', function(done) { 15 | var text = 'function(age) age < 21'; 16 | 17 | var parsedGrammar = FEEL.parse(text); 18 | parsedGrammar.build().then(result => { 19 | expect(result).not.to.be.undefined; 20 | done(); 21 | }).catch(err => done(err)); 22 | }); 23 | 24 | it('Successfully creates user defined function definition', function(done) { 25 | var text = 'function(rate, term, amount) (amount*rate/12)/(1-(1+rate/12)**-term)'; 26 | 27 | var parsedGrammar = FEEL.parse(text); 28 | parsedGrammar.build().then(result => { 29 | expect(result).not.to.be.undefined; 30 | done(); 31 | }).catch(err => done(err)); 32 | }); 33 | 34 | it('Successfully creates user defined function definition', function(done) { 35 | var text = ''; 36 | 37 | var _context_text = '{ x : function(a, b, c) if a>b then c else b }'; 38 | var _text = 'x(10,5,20)'; 39 | 40 | var parsedContext = FEEL.parse(_context_text); 41 | var parsedGrammar = FEEL.parse(_text); 42 | 43 | 44 | parsedContext.build().then(context => { 45 | parsedGrammar.build(context).then(result => { 46 | expect(result).not.to.be.undefined; 47 | done(); 48 | }).catch(err => done(err)); 49 | }).catch(err => done(err)); 50 | }); 51 | 52 | it('Successfully executes a combination of user-defined function built-in function and if expression', function(done) { 53 | 54 | var _context_text = '{ x : function(a, b, c, d) if d>a then min([d-a,b])*c else 0 }'; 55 | var _text = 'x(0,30,3,27)'; 56 | 57 | var parsedContext = FEEL.parse(_context_text); 58 | var parsedGrammar = FEEL.parse(_text); 59 | 60 | 61 | parsedContext.build().then(context => { 62 | parsedGrammar.build(context).then(result => { 63 | expect(result).not.to.be.undefined; 64 | done(); 65 | }).catch(err => done(err)); 66 | }).catch(err => done(err)); 67 | }); 68 | 69 | it('should use built-in function sum on an array', function(done) { 70 | 71 | var _context_text = '{ x : [1,2,3,4]}'; 72 | var _text = 'sum(x)'; 73 | 74 | var parsedContext = FEEL.parse(_context_text); 75 | var parsedGrammar = FEEL.parse(_text); 76 | 77 | 78 | parsedContext.build().then(context => { 79 | parsedGrammar.build(context).then(result => { 80 | expect(result).not.to.be.undefined; 81 | done(); 82 | }).catch(err => done(err)); 83 | }).catch(err => done(err)); 84 | }); 85 | 86 | it('Fails to execute a combination of user-defined function built-in function and if expression', function(done) { 87 | 88 | var context = '{getAmount : function(a,b,c,d) if d > a then min([d - a, b - a])*c else 0}'; 89 | var fn = 'getAmount(0, b, 30, d)'; 90 | 91 | var parsedContext = FEEL.parse(context); 92 | var fnCall = FEEL.parse(fn); 93 | parsedContext.build().then(ctx => { 94 | return Object.assign({}, ctx, {"d" : 300 }); 95 | }).then((context) => { 96 | return fnCall.build(context); 97 | }).then((result) => { 98 | expect(result).to.be.undefined; 99 | done(result); 100 | }).catch(err => { 101 | expect(err).not.to.be.undefined; 102 | done(); 103 | }); 104 | 105 | }); 106 | 107 | }); -------------------------------------------------------------------------------- /dmn-feel-grammar.txt: -------------------------------------------------------------------------------- 1 | 1. expression = 2 | 1.a textual expression | 3 | 1.b boxed expression ; 4 | 2. textual expression = 5 | 2.a function definition | for expression | if expression | quantified expression | 6 | 2.b disjunction | 7 | 2.c conjunction | 8 | 2.d comparison | 9 | 2.e arithmetic expression | 10 | 2.f instance of | 11 | 2.g path expression | 12 | 2.h filter expression | function invocation | 13 | 2.i literal | simple positive unary test | name | "(" , textual expression , ")" ; 14 | 3. textual expressions = textual expression , { "," , textual expression } ; 15 | 4. arithmetic expression = 16 | 4.a addition | subtraction | 17 | 4.b multiplication | division | 18 | 4.c exponentiation | 19 | 4.d arithmetic negation ; 20 | 5. simple expression = arithmetic expression | simple value ; 21 | 6. simple expressions = simple expression , { "," , simple expression } ; 22 | 7. simple positive unary test = 23 | 7.a [ "<" | "<=" | ">" | ">=" ] , endpoint | 24 | 7.b interval ; 25 | 8. interval = ( open interval start | closed interval start ) , endpoint , ".." , endpoint , ( open interval end | closed 26 | interval end ) ; 27 | 9. open interval start = "(" | "]" ; 28 | 10. closed interval start = "[" ; 29 | 11. open interval end = ")" | "[" ; 30 | 12. closed interval end = "]" ; 31 | 13. simple positive unary tests = simple positive unary test , { "," , simple positive unary test } ; 32 | 14. simple unary tests = 33 | 14.a simple positive unary tests | 34 | 14.b "not", "(", simple positive unary tests, ")" | 35 | 14.c "-"; 36 | 15. positive unary test = simple positive unary test | "null" ; 37 | 16. positive unary tests = positive unary test , { "," , positive unary test } ; 38 | 17. unary tests = 39 | 17.a positive unary tests | 40 | 17.b "not", " (", positive unary tests, ")" | 41 | 17.c "-" 42 | 18. endpoint = simple value ; 43 | 19. simple value = qualified name | simple literal ; 44 | 20. qualified name = name , { "." , name } ; 45 | 21. addition = expression , "+" , expression ; 46 | 22. subtraction = expression , "-" , expression ; 47 | 23. multiplication = expression , "*" , expression ; 48 | 24. division = expression , "/" , expression ; 49 | 25. exponentiation = expression, "**", expression ; 50 | 26. arithmetic negation = "-" , expression ; 51 | 27. name = name start , { name part | additional name symbols } ; 52 | 28. name start = name start char, { name part char } ; 53 | 29. name part = name part char , { name part char } ; 54 | 30. name start char = "?" | [A-Z] | "_" | [a-z] | [\uC0-\uD6] | [\uD8-\uF6] | [\uF8-\u2FF] | [\u370-\u37D] | 55 | [\u37F-\u1FFF] | [\u200C-\u200D] | [\u2070-\u218F] | [\u2C00-\u2FEF] | [\u3001-\uD7FF] | [\uF900-\uFDCF] | 56 | [\uFDF0-\uFFFD] | [\u10000-\uEFFFF] ; 57 | 31. name part char = name start char | digit | \uB7 | [\u0300-\u036F] | [\u203F-\u2040] ; 58 | 59 | 32. additional name symbols = "." | "/" | "-" | "’" | "+" | "*" ; 60 | 33. literal = simple literal | "null" ; 61 | 34. simple literal = numeric literal | string literal | Boolean literal | date time literal ; 62 | 35. string literal = '"' , { character – ('"' | vertical space) }, '"' ; 63 | 36. Boolean literal = "true" | "false" ; 64 | 37. numeric literal = [ "-" ] , ( digits , [ ".", digits ] | "." , digits ) ; 65 | 38. digit = [0-9] ; 66 | 39. digits = digit , {digit} ; 67 | 40. function invocation = expression , parameters ; 68 | 41. parameters = "(" , ( named parameters | positional parameters ) , ")" ; 69 | 42. named parameters = parameter name , ":" , expression , 70 | { "," , parameter name , ":" , expression } ; 71 | 43. parameter name = name ; 72 | 44. positional parameters = [ expression , { "," , expression } ] ; 73 | 45. path expression = expression , "." , name ; 74 | 46. for expression = "for" , name , "in" , expression { "," , name , "in" , expression } , "return" , expression ; 75 | 47. if expression = "if" , expression , "then" , expression , "else" expression ; 76 | 48. quantified expression = ("some" | "every") , name , "in" , expression , { name , "in" , expression } , "satisfies" , 77 | expression ; 78 | 49. disjunction = expression , "or" , expression ; 79 | 50. conjunction = expression , "and" , expression ; 80 | 51. comparison = 81 | 51.a expression , ( "=" | "!=" | "<" | "<=" | ">" | ">=" ) , expression | 82 | 51.b expression , "between" , expression , "and" , expression | 83 | 51.c expression , "in" , positive unary test ; 84 | 51.d expression , "in" , " (", positive unary tests, ")" ; 85 | 52. filter expression = expression , "[" , expression , "]" ; 86 | 53. instance of = expression , "instance" , "of" , type ; 87 | 54. type = qualified name ; 88 | 55. boxed expression = list | function definition | context ; 89 | 56. list = "[" [ expression , { "," , expression } ] , "]" ; 90 | 57. function definition = "function" , "(" , [ formal parameter { "," , formal parameter } ] , ")" , 91 | [ "external" ] , expression ; 92 | 58. formal parameter = parameter name ; 93 | 59. context = "{" , [context entry , { "," , context entry } ] , "}" ; 94 | 60. context entry = key , ":" , expression ; 95 | 61. key = name | string literal ; 96 | 62. date time literal = ( "date" | "time" | "date and time" | "duration" ) , "(" , string literal , ")" ; -------------------------------------------------------------------------------- /test/decision-service/basic-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | // const XLSX = require('xlsx'); 8 | const chai = require('chai'); 9 | // process.on('error', (e) => console.error(e)) 10 | const expect = chai.expect; 11 | const DL = require('../../utils/helper/decision-logic'); 12 | // const fs = require('fs'); 13 | 14 | const testDataFile = 'test/data/CustomerDiscount2.xlsx'; 15 | // var testDataFile2 = 'test/data/ApplicantData.xlsx'; 16 | // eslint-disable-next-line no-undef 17 | describe('additional decision table parsing logic...', () => { 18 | // eslint-disable-next-line no-undef 19 | it('should create decision table with input values list and output values list', () => { 20 | const generateContextString = DL._.generateContextString; 21 | const { parseXLS, parseCsv } = DL._; 22 | const jsonCsvObject = parseCsv(parseXLS(testDataFile)); 23 | // const values = Object.values(jsonCsvObject); 24 | const values = Object.keys(jsonCsvObject).map(k => jsonCsvObject[k]); 25 | // console.log(jsonCsvObject) 26 | expect(values.length).to.equal(1); 27 | // debugger; 28 | const result = DL._.makeContext(values[0]); 29 | 30 | expect(result.qn).to.equal('Customer Discount'); 31 | 32 | const computedExpression = result.expression; 33 | const ruleList = [ 34 | ['"Business"', '< 10', '0.1'], 35 | ['"Business"', '>=10', '0.15'], 36 | ['"Private"', '-', '0.05'], 37 | ]; 38 | 39 | // var inpValuesList = ['"Business", "Private"', '<10, >=10']; 40 | const inpValuesList = [['"Business"', '"Private"'], ['<10', '>=10']]; 41 | // var outputValuesList = ['0.05, 0.10, 0.15']; 42 | const outputValuesList = [['0.05', '0.10', '0.15']]; 43 | 44 | const contextEntries = [ 45 | 'outputs : "Discount"', 46 | 'input expression list : ' + '[\'Customer\',\'Order Size\']', 47 | { 48 | 'rule list': generateContextString(ruleList.map(r => generateContextString(r, false)), 'csv'), 49 | }, 50 | { 51 | id: "\'Customer Discount\'", 52 | }, 53 | 'hit policy: "U"', 54 | { 55 | 'input values list': generateContextString(inpValuesList.map(cl => generateContextString(cl, false)), 'csv'), 56 | 'output values': generateContextString(outputValuesList.map(cl => generateContextString(cl, false)), 'csv'), 57 | }, 58 | ]; 59 | 60 | const entry = `decision table(${generateContextString(contextEntries, 'list')})`; 61 | // var finalCtxEntry = { result: entry }; 62 | 63 | // var expectedContextString = generateContextString(finalCtxEntry, false); 64 | expect(computedExpression).to.equal(entry); 65 | }); 66 | 67 | // eslint-disable-next-line no-undef 68 | it('should generate decision table expression with multiple outputs', () => { 69 | const file = 'test/data/RoutingRules.xlsx'; 70 | const jsonFeel = DL.parseWorkbook(file); 71 | const values = Object.keys(jsonFeel).map(k => jsonFeel[k]); 72 | const computedExpression = values[0]; 73 | const outputs = ['Routing', 'Review level', 'Reason']; 74 | 75 | const routingOutputValues = ['"Decline"', '"Refer"', '"Accept"']; 76 | const reviewLevelOutputValues = ['"Level2"', '"Level1"', '"None"']; 77 | 78 | const outputValuesList = [routingOutputValues, reviewLevelOutputValues, []]; 79 | 80 | const inputValuesList = [[], ['"Low"', '"Medium"', '"High"'], []]; 81 | 82 | const qualifiedName = 'Routing rules'; 83 | const inputExpressionList = ['Age', 'Risk category', 'Debt review']; 84 | 85 | const ruleList = [ 86 | ['-', '-', '-', '"Accept"', '"None"', '"Acceptable"'], 87 | ['<18', '-', '-', '"Decline"', '"None"', '"Applicant too young"'], 88 | ['-', '"High"', '-', '"Refer"', '"Level1"', '"High risk application"'], 89 | ['-', '-', 'true', '"Refer"', '"Level2"', '"Applicant under debt review"'], 90 | ]; 91 | 92 | const hitPolicy = 'O'; 93 | 94 | const generateContextString = DL._.generateContextString; 95 | 96 | const ruleListString = generateContextString(ruleList.map(r => generateContextString(r, false)), 'csv'); 97 | const inputValuesListString = generateContextString(inputValuesList.map(r => generateContextString(r, false)), 'csv'); 98 | const outputValuesListString = generateContextString(outputValuesList.map(r => generateContextString(r, false)), 'csv'); 99 | const outputsString = generateContextString(outputs, false); 100 | const inputExpressionListString = generateContextString(inputExpressionList, false); 101 | const dtEntries = [ 102 | `outputs : ${outputsString}`, 103 | `input expression list : ${inputExpressionListString}`, 104 | `rule list : ${ruleListString}`, 105 | `id : '${qualifiedName}'`, 106 | `hit policy: "${hitPolicy}"`, 107 | `input values list : ${inputValuesListString}`, 108 | `output values : ${outputValuesListString}`, 109 | ]; 110 | 111 | const expectedExpression = `decision table(${generateContextString(dtEntries, 'list')})`; 112 | 113 | expect(computedExpression).to.equal(expectedExpression); 114 | }); 115 | }); 116 | -------------------------------------------------------------------------------- /utils/built-in-functions/date-time-functions/time.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | /* 9 | Decision Model and Notation, v1.1 10 | Page : 131-132 11 | */ 12 | 13 | /* 14 | Format : time(from) // from - time string 15 | Description : time string convert "from" to time 16 | e.g. : time("23:59:00z") + duration("PT2M") = time("00:01:00@Etc/UTC") 17 | */ 18 | 19 | /* 20 | Format : time(from) // from - time, date_and_time 21 | Description : time, date and time convert "from" to time (ignoring date components) 22 | e.g. : time(date and time("2012-12-25T11:00:00Z")) = time("11:00:00Z") 23 | */ 24 | 25 | /* 26 | Format : time(hour, minute, second, offset) 27 | Description : hour, minute, second, are numbers, offset is a days and time duration, or null creates a time from the given component values 28 | e.g. : time(“T23:59:00z") = time(23, 59, 0, duration(“PT0H”)) 29 | */ 30 | 31 | const moment = require('moment-timezone'); 32 | const addProperties = require('./add-properties'); 33 | const { time_ISO_8601, time_IANA_tz, types, properties } = require('../../helper/meta'); 34 | const { duration } = require('./duration'); 35 | 36 | const { hour, minute, second, 'time offset': time_offset, timezone } = properties; 37 | const props = Object.assign({}, { hour, minute, second, 'time offset': time_offset, timezone, type: types.time, isTime: true }); 38 | 39 | const isNumber = args => args.reduce((prev, next) => prev && typeof next === 'number', true); 40 | 41 | const parseTime = (str) => { 42 | try { 43 | const t = moment.parseZone(str, time_ISO_8601); 44 | if (t.isValid()) { 45 | return t; 46 | } 47 | throw new Error('Invalid ISO_8601 format time. This is usually caused by an inappropriate format. Please check the input format.'); 48 | } catch (err) { 49 | throw err; 50 | } 51 | }; 52 | 53 | const dtdToOffset = (dtd) => { 54 | const msPerDay = 86400000; 55 | const msPerHour = 3600000; 56 | const msPerMinute = 60000; 57 | let d = dtd; 58 | if (typeof dtd === 'number') { 59 | const ms = Math.abs(dtd); 60 | let remaining = ms % msPerDay; 61 | const hours = remaining / msPerHour; 62 | remaining %= msPerHour; 63 | const minutes = remaining / msPerMinute; 64 | d = duration(`PT${hours}H${minutes}M`); 65 | } 66 | if (d.isDtd) { 67 | let { hours, minutes } = d; 68 | hours = hours < 10 ? `0${hours}` : `${hours}`; 69 | minutes = minutes < 10 ? `0${minutes}` : `${minutes}`; 70 | return `${hours}:${minutes}`; 71 | } 72 | throw new Error('Invalid Type'); 73 | }; 74 | 75 | const parseIANATz = (str) => { 76 | const match = str.match(time_IANA_tz); 77 | if (match) { 78 | const [hour, minute, second, tz] = match.slice(1); 79 | if (hour && minute && second && tz) { 80 | try { 81 | const t = moment.tz({ hour, minute, second }, tz); 82 | if (t.isValid()) { 83 | return t; 84 | } 85 | throw new Error('Invalid IANA format time. This is usually caused by an inappropriate format. Please check the input format.'); 86 | } catch (err) { 87 | throw err; 88 | } 89 | } 90 | throw new Error(`Error parsing IANA format input. One or more parts are missing - hour : ${hour} minute : ${minute} second : ${second} timezone : ${tz}`); 91 | } 92 | return match; 93 | }; 94 | 95 | const time = (...args) => { 96 | let t; 97 | if (args.length === 1) { 98 | const arg = args[0]; 99 | if (typeof arg === 'string') { 100 | try { 101 | t = arg === '' ? moment() : parseIANATz(arg) || parseTime(arg); 102 | } catch (err) { 103 | throw err; 104 | } 105 | } else if (typeof arg === 'object') { 106 | if (arg instanceof Date) { 107 | t = moment.parseZone(arg.toISOString); 108 | } else if (arg.isDateTime) { 109 | const str = arg.format(time_ISO_8601); 110 | t = moment.parseZone(str, time_ISO_8601); 111 | } 112 | if (!t.isValid()) { 113 | throw new Error('Invalid time. Parsing error while attempting to extract time from date and time.'); 114 | } 115 | } else { 116 | throw new Error('Invalid format encountered. Please specify time in one of these formats :\n- "23:59:00z"\n- "00:01:00@Etc/UTC"\n- date_and_time object'); 117 | } 118 | } else if (args.length >= 3 && isNumber(args.slice(0, 3))) { 119 | const [hour, minute, second] = args.slice(0, 3); 120 | t = moment({ hour, minute, second }); 121 | const dtd = args[3]; 122 | if (dtd) { 123 | try { 124 | const sign = Math.sign(dtd) < 0 ? '-' : '+'; 125 | const offset = `${sign}${dtdToOffset(dtd)}`; 126 | t = moment.parseZone(`${moment({ hour, minute, second }).format('THH:mm:ss')}${offset}`, time_ISO_8601); 127 | } catch (err) { 128 | throw new Error(`${err.message} - the fourth argument in "time" in-built function is expected to be of type "days and time duration"`); 129 | } 130 | } 131 | if (!t.isValid()) { 132 | throw new Error('Invalid time. Parsing error while attempting to create time from parts'); 133 | } 134 | } else { 135 | throw new Error('Invalid number of arguments specified with "time" in-built function'); 136 | } 137 | 138 | return addProperties(t, props); 139 | }; 140 | 141 | module.exports = { time }; 142 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const gulp = require('gulp'); 9 | const concat = require('gulp-concat'); 10 | // const watch = require('gulp-watch'); 11 | const insert = require('gulp-insert'); 12 | const clean = require('gulp-clean'); 13 | const peg = require('./utils/dev/gulp-pegjs'); 14 | const minimist = require('minimist'); 15 | const gutil = require('gulp-util'); 16 | const mocha = require('gulp-mocha'); 17 | var istanbul = require('gulp-istanbul'); 18 | const eslint = require('gulp-eslint'); 19 | const through = require('through2'); 20 | 21 | const knownOptions = { 22 | string: 'expr', 23 | default: '', 24 | }; 25 | 26 | const options = minimist(process.argv.slice(2), knownOptions); 27 | 28 | const log = label => { 29 | const log = (file, enc, cb) => { 30 | console.log(`${label} : ${file.path}`); 31 | cb(null, file); 32 | }; 33 | return through.obj(log); 34 | }; 35 | 36 | gulp.task('initialize:feel', () => gulp.src('./grammar/feel-initializer.js') 37 | .pipe(insert.transform((contents, file) => { 38 | let initializer_start = '{ \n', 39 | initializer_end = '\n }'; 40 | return initializer_start + contents + initializer_end; 41 | })) 42 | .pipe(gulp.dest('./temp'))); 43 | 44 | // gulp.task('concat:feel', ['initialize:feel'] 45 | gulp.task('concat:feel', () => gulp.src(['./temp/feel-initializer.js', './grammar/feel.pegjs']) 46 | .pipe(concat('feel.pegjs')) 47 | .pipe(gulp.dest('./src/'))); 48 | 49 | // gulp.task('clean:temp', ['initialize:feel', 'concat:feel'] 50 | gulp.task('clean:temp', () => gulp.src('./temp', {read: false}) 51 | .pipe(clean())); 52 | 53 | // gulp.task('clean:dist:feel', ['src:lint'] 54 | gulp.task('clean:dist:feel', () => gulp.src('./dist/feel.js', {read: false}) 55 | .pipe(clean())); 56 | 57 | // gulp.task('clean:dist:feel:ast', ['src:lint'] 58 | gulp.task('clean:dist:feel:ast', () => gulp.src('./dist/feel-ast*.js', {read: false}) 59 | .pipe(clean())); 60 | 61 | gulp.task('clean:src:feel', () => gulp.src('./src/feel.pegjs', {read: false}) 62 | .pipe(clean())); 63 | 64 | // gulp.task('generate:parser', ['clean:dist:feel', 'concat:feel'] 65 | gulp.task('generate:parser', () => gulp.src('src/feel.pegjs') 66 | .pipe(peg({ 67 | format: 'commonjs', 68 | cache: true, 69 | allowedStartRules: ["Start", "SimpleExpressions", "UnaryTests", "SimpleUnaryTests"] 70 | })) 71 | .pipe(gulp.dest('./dist'))); 72 | 73 | // gulp.task('dist:feel:ast', ['clean:dist:feel:ast'] 74 | gulp.task('dist:feel:ast', () => gulp.src('src/feel-ast.js') 75 | .pipe(gulp.dest('./dist'))); 76 | 77 | // gulp.task('dist:feel:ast:parser', ['clean:dist:feel:ast'] 78 | gulp.task('dist:feel:ast:parser', () => gulp.src('src/feel-ast-parser.js') 79 | .pipe(gulp.dest('./dist'))); 80 | 81 | gulp.task('mocha', () => gulp.src(['test/*.js'], {read: false}) 82 | .pipe(mocha({reporter: 'list'})) 83 | .on('error', gutil.log)); 84 | 85 | gulp.task('lint', () => { 86 | return gulp.src(['**/*.js', '!node_modules/**']) 87 | .pipe(log('linting')) 88 | .pipe(eslint()) 89 | .pipe(eslint.format()) 90 | .pipe(eslint.failAfterError()); 91 | }); 92 | 93 | gulp.task('src:lint', () => { 94 | return gulp.src(['src/*.js']) 95 | .pipe(eslint()) 96 | .pipe(eslint.format()) 97 | .pipe(eslint.failAfterError()); 98 | }); 99 | 100 | gulp.task('utils:lint', () => { 101 | return gulp.src(['utils/*.js']) 102 | .pipe(eslint()) 103 | .pipe(eslint.format()) 104 | .pipe(eslint.failAfterError()); 105 | }); 106 | 107 | gulp.task('pre-test-ci', function () { 108 | return gulp.src(['./dist/**/*.js', './utils/**/*.js', '!./dist/**/feel.js', '!./utils/**/index.js']) 109 | .pipe(istanbul()) 110 | .pipe(istanbul.hookRequire()); 111 | }); 112 | 113 | gulp.task('test-ci', gulp.series('pre-test-ci', function () { 114 | return gulp.src(['test/**/*.spec.js']) 115 | .pipe(mocha()) 116 | .pipe(istanbul.writeReports({ 117 | dir: './coverage', 118 | reporters: ['lcovonly'], 119 | reportOpts: { dir: './coverage' } 120 | })) 121 | .pipe(istanbul.enforceThresholds({ thresholds: { global: { statements: 85, branches: 70, lines: 85, functions: 90 } } })); 122 | })); 123 | 124 | gulp.task('test-ci-html', gulp.series('pre-test-ci', function () { 125 | return gulp.src(['test/**/*.spec.js']) 126 | .pipe(mocha()) 127 | .pipe(istanbul.writeReports({ 128 | dir: './coverage', 129 | reporters: ['lcov'], 130 | reportOpts: { dir: './coverage' } 131 | })) 132 | .pipe(istanbul.enforceThresholds({ thresholds: { global: { statements: 85, branches: 70, lines: 85, functions: 90 } } })); 133 | })); 134 | 135 | // ['initialize:feel', 'clean:src:feel', 'concat:feel', 'clean:temp'] 136 | gulp.task('build', gulp.series('initialize:feel', 'clean:src:feel', 'concat:feel', 'clean:temp')); 137 | 138 | // ['build', 'generate:parser', 'mocha'] 139 | gulp.task('default', gulp.series('initialize:feel', 'clean:src:feel', 'src:lint', 'concat:feel', 'clean:temp', 'clean:dist:feel', 'generate:parser', 'mocha')); 140 | 141 | // gulp.task('watch', () => { 142 | // gulp.watch('./grammar/*', ['build']); 143 | // gulp.watch('./src/*.pegjs', ['generate:parser']); 144 | // gulp.watch('./src/*.js', ['dist:feel:ast', 'dist:feel:ast:parser']); 145 | // gulp.watch('./utils/**/*.js', ['utils:lint']); 146 | // }); 147 | 148 | // ['build', 'dist:feel:ast', 'dist:feel:ast:parser', 'generate:parser'] 149 | gulp.task('dist', gulp.series('initialize:feel', 'clean:src:feel', 'src:lint', 'concat:feel', 'clean:temp', 'clean:dist:feel', 'generate:parser', 'clean:dist:feel:ast', 'dist:feel:ast', 'dist:feel:ast:parser')); -------------------------------------------------------------------------------- /test/decision-service/individual-sheets-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | const DL = require('../../utils/helper/decision-logic'); 8 | const DS = require('../../utils/helper/decision-service'); 9 | const expect = require('chai').expect; 10 | 11 | describe('individual sheets...', function(){ 12 | var i = 0; 13 | var path; 14 | var xlArr = [ 15 | 'ExamEligibility.xlsx', 16 | 'Adjustments2.xlsx', 17 | 'Applicant_Risk_Rating.xlsx', 'ApplicantRiskRating.xlsx', 18 | 'Discount.xlsx', 'ElectricityBill.xlsx', 19 | 'Holidays.xlsx', 20 | 'PostBureauRiskCategory.xlsx', 21 | 'RoutingRules.xlsx', 'StudentFinancialPackageEligibility.xlsx' 22 | ]; 23 | 24 | beforeEach('load test file', function(){ 25 | path = 'test/data/' + xlArr[i++]; 26 | }); 27 | 28 | var { executeDecisionService, createDecisionGraphAST } = DS; 29 | var { parseWorkbook } = DL; 30 | 31 | var runTest = function(path, name, payload, suiteCb, testCb) { 32 | var decisionMap = parseWorkbook(path); 33 | var ast = createDecisionGraphAST(decisionMap); 34 | return executeDecisionService(ast, name, payload, 'a' + i) 35 | // .then(result => testCb(result)) 36 | // .catch(suiteCb); 37 | }; 38 | it('ExamEligibility.xlsx', function(done) { 39 | var payload = { 40 | GPA: 7, 41 | d: "1995-11-22" 42 | }; 43 | 44 | runTest(path, 'ExamEligibility', payload) 45 | .then(results => { 46 | expect(results.Eligible).to.be.true; 47 | done(); 48 | }) 49 | .catch(done); 50 | }); 51 | 52 | it('Adjustments2.xlsx', function(done){ 53 | var payload = { 54 | Customer: "Private", 55 | "Order size": 12 56 | }; 57 | debugger; 58 | runTest(path, 'Adjustments', payload).then(result => { 59 | expect(result.Shipping).to.equal('Air'); 60 | expect(result.Discount).to.equal(0.05); 61 | done(); 62 | }) 63 | .catch(done); 64 | }); 65 | 66 | it('Applicant_Risk_Rating.xlsx', function(done){ 67 | var payload = { 68 | "Applicant Age" : 25, 69 | "Medical History" : "good" 70 | }; 71 | 72 | runTest(path, 'Applicant_Risk_Rating', payload) 73 | .then(result => { 74 | expect(result['Applicant Risk Rating']) 75 | .to.equal('Medium'); 76 | done(); 77 | }) 78 | .catch(done); 79 | }); 80 | 81 | it('ApplicantRiskRating.xlsx', function(done) { 82 | var payload = { 83 | "Applicant Age" : -24, 84 | "Medical History" : "bad" 85 | }; 86 | 87 | runTest(path, 'Applicant Risk Rating', payload) 88 | .then(result => { 89 | expect(result['Applicant Risk Rating']).to.equal('Medium'); 90 | done(); 91 | }) 92 | .catch(done); 93 | }); 94 | 95 | it('Discount.xlsx', function(done) { 96 | var payload = { "Customer" : "Business", "Order size" : 10 }; 97 | runTest(path, 'Discount', payload) 98 | .then(result => { 99 | expect(result.Discount).to.equal(0.15); 100 | done(); 101 | }) 102 | .catch(done); 103 | }); 104 | 105 | it(`ElectricityBill.xlsx`, function(done) { //electricity bill 106 | var payload = { "State" : "Karnataka", "Units" : 31 }; 107 | debugger; 108 | runTest(path, 'Electricity Bill', payload) 109 | .then(results => { 110 | expect(results.Amount).to.equal(94.4); 111 | done(); 112 | }) 113 | .catch(done); 114 | }); 115 | 116 | it(`Holidays.xlsx`, function(done) { //Holidays 117 | var payload = { "Age" : 100, "Years of Service" : 200 }; 118 | runTest(path, 'Holidays', payload) 119 | .then(results => { 120 | 121 | expect(results.length).to.equal(5); 122 | expect(results[0].Holidays).to.equal(22); 123 | expect(results[1].Holidays).to.equal(5); 124 | expect(results[2].Holidays).to.equal(5); 125 | expect(results[3].Holidays).to.equal(3); 126 | expect(results[4].Holidays).to.equal(3); 127 | done(); 128 | 129 | }) 130 | .catch(done); 131 | }); 132 | 133 | it(`PostBureauRiskCategory.xlsx`, function(done) { //PostBureauRiskCategory 134 | var payload = {"Applicant": {"ExistingCustomer" : true}, "Report": {"CreditScore" : 600}, "b" : 60}; 135 | runTest(path, 'PostBureauRiskCategory', payload) 136 | .then(results => { 137 | expect(results.PostBureauRiskCategory).to.equal('MEDIUM'); 138 | done(); 139 | 140 | }) 141 | .catch(done); 142 | }); 143 | 144 | it('RoutingRules.xlsx', function(done) { 145 | var payload = { 146 | "Age" : 18, 147 | "Risk category" : "High", 148 | "Debt review" : false 149 | }; 150 | 151 | runTest(path, 'Routing rules', payload) 152 | .then(results => { 153 | expect(results.length).to.equal(2); 154 | expect(results[0].Routing).to.equal('Refer'); 155 | expect(results[0]['Review level']).to.equal('Level1'); 156 | expect(results[0].Reason).to.equal('High risk application'); 157 | expect(results[1].Routing).to.equal('Accept'); 158 | expect(results[1]['Review level']).to.equal('None'); 159 | expect(results[1].Reason).to.equal('Acceptable'); 160 | done(); 161 | }) 162 | .catch(done) 163 | }); 164 | 165 | it('StudentFinancialPackageEligibility.xlsx', function(done) { 166 | var payload = { 167 | "Student GPA" : 3.6, 168 | "Student Extra-Curricular Activities Count" : 4, 169 | "Student National Honor Society Membership" : "Yes" 170 | }; 171 | 172 | runTest(path,'Student Financial Package Eligibility', payload) 173 | .then(results => { 174 | expect(results.length).to.equal(2); 175 | expect(results[0]['Student Financial Package Eligibility List']).to.equal('20% Scholarship'); 176 | expect(results[1]['Student Financial Package Eligibility List']).to.equal('30% Loan'); 177 | done(); 178 | }) 179 | .catch(done) 180 | }); 181 | 182 | 183 | }); 184 | -------------------------------------------------------------------------------- /INDIVIDUAL_CLA.md: -------------------------------------------------------------------------------- 1 | # Individual Contributor License Agreement 2 | 3 | By signing this Individual Contributor License Agreement (“Agreement”), and making a Contribution (as defined below) to EdgeVerve Systems Limited, located in Electronics City, Hosur Road, Bangalore 560 100 (“EdgeVerve”), You (as defined below) accept and agree to the following terms and conditions for Your present and future Contributions submitted to EdgeVerve. Except for the license granted in this Agreement to EdgeVerve and recipients of software distributed by EdgeVerve, You reserve all right, title, and interest in and to Your Contributions. Please read this document carefully before signing and keep a copy for Your records. Please email a signed .pdf file of this Agreement to IPC@edgeverve.com. 4 | 1. Definitions: 5 | “You” or “Your” shall mean the copyright owner or the individual authorized by the copyright owner that is entering into this Agreement with EdgeVerve. 6 | “Contribution” shall mean any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to EdgeVerve for inclusion in, or documentation of, any of the products owned or managed by EdgeVerve (“Work”). For purposes of this definition, “submitted” means any form of electronic, verbal, or written communication sent to EdgeVerve or its representatives, including but not limited to communication or electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, EdgeVerve for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as “Not a Contribution.” 7 | 2. Grant of Copyright License: 8 | Subject to the terms and conditions of this Agreement, You hereby grant EdgeVerve and recipients of software distributed by EdgeVerve, a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works under any license and without any restrictions. 9 | 3. Grant of Patent License: 10 | Subject to the terms and conditions of this Agreement, You hereby grant to EdgeVerve and to recipients of software distributed by EdgeVerve a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this Section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work under any license and without any restrictions. The patent license You grant to EdgeVerve and the recipients of the software under this Section applies only to those patent claims licensable by You that are necessarily infringed by Your Contributions(s) alone or by combination of Your Contributions(s) with the Work to which such Contribution(s) was submitted. If any entity institutes a patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 11 | 4. Grant of License: 12 | You represent that You are legally entitled to grant the licenses under this Agreement. 13 | If Your employer(s) has rights to intellectual property that You create, You represent that You have received appropriate permission(s) to make the Contributions on behalf of that employer, that Your employer has waived such rights for Your Contributions, or that Your employer has executed a separate Corporate Contributor License Agreement with EdgeVerve. 14 | 5. Original Work: 15 | You represent that each of Your Contributions are Your original works of authorship (see Section 7 - Submission on behalf of others). You represent that to Your knowledge, no other person claims, or has the right to claim, any right in any intellectual property right related to Your Contributions. 16 | You also represent that You are not legally obligated, whether by entering into an agreement or otherwise, in any way that conflicts with the terms of this Agreement. 17 | You represent that Your Contribution submissions include complete details of any third-party license or other restriction (including, but not limited to, related patents and trademarks) of which You are personally aware and which are associated with any part of Your Contributions. 18 | 6. Support: 19 | You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. EdgeVerve acknowledges that unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. 20 | 7. Submission on behalf of others: 21 | If You wish to submit work that is not Your original creation, You may submit it to EdgeVerve separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which You are personally aware, and conspicuously marking the work as “Submitted on Behalf of a Third-Party: [named here]”. 22 | 8. Change of circumstances: 23 | You agree to notify EdgeVerve of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect. Email us at IPC@edgeverve.com. 24 |
25 |
26 |
27 | 28 | 29 | Signature: ____________________________________________________________________ 30 | 31 | 32 | Name: _________________________________________________________________________ 33 | 34 | 35 | Title: _________________________________________________________________________ 36 | 37 | 38 | Date: _________________________________________________________________________ 39 | 40 | -------------------------------------------------------------------------------- /CORPORATE_CLA.md: -------------------------------------------------------------------------------- 1 | # Corporate Contributor License Agreement 2 | 3 | By signing this Corporate Contributor License Agreement (“Agreement”), and making a Contribution (as defined below) to EdgeVerve Systems Limited, located in Electronics City, Hosur Road, Bangalore 560 100 (“EdgeVerve”). 4 | 5 | This version of the Agreement allows an entity (the "Corporation") to submit Contributions to EdgeVerve, to authorize Contributions submitted by its designated employees to EdgeVerve, and to grant copyright and patent licenses thereto. 6 | 7 | You (as defined below) accept and agree to the following terms and conditions for Your present and future Contributions submitted to EdgeVerve. Except for the license granted in this Agreement to EdgeVerve and recipients of software distributed by EdgeVerve, You reserve all right, title, and interest in and to Your Contributions. Please read this document carefully before signing and keep a copy for your records. Please email a signed .pdf file of this Agreement to IPC@edgeverve.com. 8 | 1. Definitions: 9 | “You” or “Your” shall mean the copyright owner or the legal entity authorized by the copyright owner that is entering into this Agreement with EdgeVerve. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. 10 | "Contribution" shall mean the code, documentation or other original works of authorship expressly identified in Schedule B, as well as any original work of authorship, including any modifications or additions to an existing work, that is intentionally submitted by You to the EdgeVerve for inclusion in, or documentation of, any of the products owned or managed by the EdgeVerve ("Work"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the EdgeVerve or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, EdgeVerve for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." 11 | 2. Grant of Copyright License: 12 | Subject to the terms and conditions of this Agreement, You hereby grant EdgeVerve and recipients of software distributed by EdgeVerve, a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works under any license and without any restrictions. 13 | 3. Grant of Patent License: 14 | Subject to the terms and conditions of this Agreement, You hereby grant to EdgeVerve and to recipients of software distributed by EdgeVerve a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this Section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work under any license and without any restrictions. The patent license You grant to EdgeVerve and the recipients of the software under this Section applies only to those patent claims licensable by You that are necessarily infringed by Your Contributions(s) alone or by combination of Your Contributions(s) with the Work to which such Contribution(s) was submitted. If any entity institutes a patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. 15 | 4. Grant of License: 16 | You represent that You are legally entitled to grant the licenses under this Agreement. 17 | You represent further that every employee of the Corporation mentioned in Schedule A below (or in a subsequent written modification to that Schedule) is authorized to submit Contributions on behalf of the Corporation 18 | 5. Original Work: 19 | You represent that each of Your Contributions are Your original works of authorship (see Section 7 - Submission on behalf of others) 20 | 6. Support: 21 | You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. EdgeVerve acknowledges that unless required by applicable law or agreed to in writing, You provide Your Contributions on an “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE. 22 | 7. Submission on behalf of others: 23 | If You wish to submit work that is not Your original creation, You may submit it to EdgeVerve separately from any Contribution, identifying the complete details of its source and of any license or other restriction (including, but not limited to, related patents, trademarks, and license agreements) of which You are personally aware, and conspicuously marking the work as “Submitted on Behalf of a Third-Party: [named here]”. 24 | 8. Change of circumstances: 25 | It is your responsibility to notify EdgeVerve when any change is required to the list of designated employees authorized to submit Contributions on behalf of the Corporation, or to the Corporation's Point of Contact with EdgeVerve. You agree to notify EdgeVerve of any facts or circumstances of which You become aware that would make these representations inaccurate in any respect. Email us at IPC@edgeverve.com. 26 | 27 |
28 | 29 | [Signature page follows] 30 |
31 |
32 |
33 | 34 | Signature: _____________________________________________________________________ 35 | 36 | 37 | Name: __________________________________________________________________________ 38 | 39 | 40 | Corporation: ___________________________________________________________________ 41 | 42 | 43 | Title: _________________________________________________________________________ 44 | 45 | 46 | Date: __________________________________________________________________________ 47 | 48 | 49 | 50 | 51 | 52 | Schedule A 53 | List of employees 54 | 55 | Schedule B 56 | List of works of authorship -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/EdgeVerve/feel.svg?branch=master)](https://travis-ci.org/EdgeVerve/feel) [![Coverage Status](https://coveralls.io/repos/github/EdgeVerve/feel/badge.svg?branch=master)](https://coveralls.io/github/EdgeVerve/feel?branch=master) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![npm](https://img.shields.io/npm/v/js-feel.svg)](https://npmjs.org/package/js-feel) 2 | 3 | [![NPM](https://nodei.co/npm/js-feel.png?compact=true)](https://npmjs.org/package/js-feel) 4 | 5 | # About 6 | 7 | [FEEL](https://github.com/EdgeVerve/feel/wiki/What-is-FEEL%3F) is an expression language based on DMN specification conformance level 3. 8 | Written using [PEG.js](https://pegjs.org/) - JavaScript Parser Generator. 9 | FEEL is a very powerful language built with the purpose of defining rules in Business Rule Engines. 10 | FEEL also offers an API to implement and execute Decision Table defined in excel (.xlsx) 11 | 12 | # Getting Started 13 | 14 | FEEL is a completely flexible library which can be used with any project to add support for *Decision Table*. It also comes with a powerful expression language termed *FEEL* built-in to define a multitude of decision rules. 15 | 16 | ## Installation 17 | 18 | ### Development 19 | 20 | ```sh 21 | # npm install 22 | npm install js-feel --save 23 | 24 | ``` 25 | 26 | ### Contribution 27 | 28 | ```sh 29 | # clone repo 30 | git clone https://github.com/EdgeVerve/feel.git 31 | 32 | # or fork repo 33 | 34 | # install dependencies 35 | npm install 36 | 37 | # run test cases 38 | npm test 39 | 40 | # watch for changes in source and grammar 41 | gulp watch 42 | 43 | # generate parser from grammar 44 | gulp generate 45 | 46 | # lint source files 47 | npm run lint 48 | 49 | # lint-fix source files 50 | npm run lintfix 51 | ``` 52 | 53 | # Usage 54 | 55 | ## Using [Decision Table](https://github.com/EdgeVerve/feel/wiki/Decision-Table#what-is-decision-table) 56 | 57 | Decision tables are defined in excel (.xlsx). Please check [Sample Rules](README.md#sample-rules). 58 | Each cell in the body of the decision table has to be a valid FEEL expression. The following make use of FEEL parser to parse and execute expressions and hence the decision logic. 59 | 60 | ### Excel to Decision Table 61 | 62 | ```javascript 63 | const { decisionTable } = require('feel')(); 64 | 65 | const csv = decisionTable.xls_to_csv('./test/StudentFinancialPackageEligibility.xlsx'); 66 | const decision_table = decisionTable.csv_to_decision_table(csv[0]); 67 | ``` 68 | 69 | ### Execute Decision Table 70 | 71 | The Decision Table (decision_table) created in the previous step can be executed using; *decisionTable.execute_decision_table* 72 | 73 | ```javascript 74 | 75 | const payload = {"Student GPA" : 3.6,"Student Extra-Curricular Activities Count" : 4,"Student National Honor Society Membership" : "Yes"}; 76 | decisionTable.execute_decision_table("StudentFinancialPackageEligibility", decision_table,payload, (results)=> { 77 | console.log(results) 78 | }); 79 | ``` 80 | 81 | ## Using [FEEL](https://github.com/EdgeVerve/feel/wiki/What-is-FEEL%3F) Standalone 82 | 83 | ```javascript 84 | const {feel} = require('feel')(); 85 | 86 | const rule = 'a + b - c'; 87 | const context = { 88 | a: 10, 89 | b: 20, 90 | c: 5 91 | }; 92 | 93 | const parsedGrammar = feel.parse(rule); 94 | parsedGrammar.build(context).then(result => { 95 | console.log(result); 96 | }).catch(err => console.error(err)); 97 | ``` 98 | 99 | # Sample FEEL Expressions 100 | 101 | Some valid FEEL expressions (logically categorized): 102 | 103 | ### Arithmetic 104 | 105 | - a + b - c 106 | - ((a + b)/c - (d + e*2))**f 107 | - 1-(1+rate/12)**-term 108 | - (a + b)**-c 109 | - date("2012-12-25") + date("2012-12-24") 110 | - time("T13:10:06") - time("T13:10:05") 111 | - date and time("2012-12-24T23:59:00") + duration("P1Y") 112 | 113 | ### Comparision 114 | 115 | - 5 in (<= 5) 116 | - 5 in ((5..10]) 117 | - 5 in ([5..10]) 118 | - 5 in (4,5,6) 119 | - 5 in (<5,>5) 120 | - (a + 5) >= (7 + g) 121 | - (a+b) between (c + d) and (e - f) 122 | - date("2012-12-25") > date("2012-12-24") 123 | - date and time("2012-12-24T23:59:00") < date and time("2012-12-25T00:00:00") 124 | 125 | ### Conjunction 126 | 127 | - a or b 128 | - a and b 129 | - ((a or b) and (b or c)) or (a and d) 130 | - ((a > b) and (a > c)) and (b > c) 131 | - ((a + b) > (c - d)) and (a > b) 132 | - a or b or a > b 133 | - (x(i, j) = y) and (a > b) 134 | - (a + b) > (c - d) and (a > b) 135 | 136 | ### For 137 | 138 | - for a in [1,2,3] return a * a 139 | - for age in [18..40], name in ["george", "mike", "bob"] return status 140 | 141 | ### Function Definition 142 | 143 | - function(age) age < 21 144 | - function(rate, term, amount) (amount*rate/12)/(1-(1+rate/12)**-term) 145 | 146 | ### If 147 | 148 | - if applicant.maritalStatus in ("M", "S") then "valid" else "not valid" 149 | - if Pre-Bureau Risk Category = "DECLINE" or Installment Affordable = false or Age < 18 or Monthly Income < 100 then "INELIGIBLE" else "ELIGIBLE" 150 | - if "Pre-Bureau Risk Category" = "DECLINE" or "Installment Affordable" = false or Age < 18 or "Monthly Income" < 100 then "INELIGIBLE" else "ELIGIBLE" 151 | 152 | ### Quantified 153 | 154 | - some ch in credit history satisfies ch.event = "bankruptcy" 155 | 156 | ### Date Time Semantics 157 | 158 | - time("13:10:05@Etc/UTC").hour 159 | - time("13:10:05@Etc/UTC").minute 160 | - time("13:01:05+05:30").second 161 | - date and time("2012-12-24T23:59:00").year 162 | - date("2017-06-10").month 163 | - date("2017-06-10").day 164 | - duration("P13M").years 165 | - duration("P1Y11M").months 166 | - duration("P5DT12H10M").days 167 | - duration("P5DT12H10M").hours 168 | - duration("P5DT12H10M").minutes 169 | - duration("P5DT12H10M25S").seconds 170 | 171 | ### Date Time Conversion and Equality 172 | 173 | - date("2012-12-25") – date("2012-12-24") = duration("P1D") 174 | - date and time("2012-12-24T23:59:00") + duration("PT1M") = date and time("2012-12-25T00:00:00") 175 | - time("23:59:00z") + duration("PT2M") = time("00:01:00@Etc/UTC") 176 | - date and time("2012-12-24T23:59:00") - date and time("2012-12-22T03:45:00") = duration("P2DT20H14M") 177 | - duration("P2Y2M") = duration("P26M") 178 | 179 | ***Please note: This is not a complete list of FEEL Expressions. Please refer [DMN Specification Document](http://www.omg.org/spec/DMN/1.1/) for detailed documentation on FEEL grammar.*** 180 | 181 | # Sample Rules 182 | 183 | [Validation.xlsx](/test/data/Validation.xlsx) 184 | 185 | [PostBureauRiskCategory.xlsx](/test/data/PostBureauRiskCategory.xlsx) 186 | 187 | [ElectricityBill.xlsx](/test/data/ElectricityBill.xlsx) 188 | 189 | # Reference 190 | 191 | For comprehensive set of documentation on DMN, you can refer to : 192 | 193 | [DMN Specification Document](http://www.omg.org/spec/DMN/1.1/) 194 | -------------------------------------------------------------------------------- /test/decision-service/pegjs-tests.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var XLSX = require('xlsx'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DL = require('../../utils/helper/decision-logic'); 11 | var fs = require('fs'); 12 | var FEEL = require('../../dist/feel'); 13 | var chalk = require('chalk'); 14 | 15 | var DS = require('../../utils/helper/decision-service'); 16 | 17 | describe(chalk.blue('Pegjs parsing tests...'), function(){ 18 | it('should successfully parse a excel workbook - decision service', function() { 19 | var file = 'test/data/RoutingDecisionService.xlsx'; 20 | debugger; 21 | var jsonFeel = DL.parseWorkbook(file); 22 | 23 | var keys = Object.keys(jsonFeel) 24 | 25 | keys.forEach(function(key) { 26 | var feelExpression = jsonFeel[key]; 27 | try { 28 | var grammer = FEEL.parse(feelExpression); 29 | } 30 | catch(e) { 31 | expect(true, JSON.stringify({key: key, feel: feelExpression, error: e.message}, null, 2)).to.be.false; 32 | } 33 | 34 | expect(grammer, 'For name: ' + key).not.to.be.undefined; 35 | }); 36 | }); 37 | 38 | 39 | 40 | // it('should execute a decision service given test data', function(done){ 41 | // const { createDecisionGraphAST, executeDecisionService } = require('../../utils/helper/decision-service.js'); 42 | 43 | // const inputData = { 44 | // 'Applicant data': { 45 | // Age: 51, 46 | // MaritalStatus: 'M', 47 | // EmploymentStatus: 'EMPLOYED', 48 | // 'ExistingCustomer': false, 49 | // 'Monthly': { 50 | // 'Income': 10000, 51 | // 'Repayments': 2500, 52 | // 'Expenses': 3000 53 | // } 54 | // }, 55 | // 'Requested product': { 56 | // ProductType: 'STANDARD LOAN', 57 | // Rate: 0.08, 58 | // Term: 36, 59 | // Amount: 100000 60 | // }, 61 | // 'Bureau data': { 62 | // Bankrupt: false, 63 | // CreditScore: 600 64 | // } 65 | // } 66 | 67 | // const decisionMap = { 68 | // "Routing": "Routing Rules (Bankrupt : Bureau data . Bankrupt,Credit Score : Bureau data . CreditScore,Post Bureau Risk Category : Post bureau risk category,Post Bureau Affordability : Post bureau affordability)", 69 | // "Routing Rules": "decision table(outputs : \"Routing\",input expression list : ['Post Bureau Risk Category','Post Bureau Affordability','Bankrupt','Credit Score'],rule list : [['-','FALSE','-','-','\"DECLINE\"'],['-','-','TRUE','-','\"DECLINE\"'],['\"HIGH\"','-','-','-','\"REFER\"'],['-','-','-','<580','\"REFER\"'],['-','-','-','-','\"ACCEPT\"']],id : 'Routing Rules',hit policy: \"P\",input values list : [[null, [0..999]]],output values : [[\"DECLINE\", \"REFER\", \"ACCEPT\"]])", 70 | // "Post bureau risk category": "Post Bureau risk category table (Existing Customer : Applicant data . ExistingCustomer,Credit Score : Bureau data . CreditScore,Application Risk Score : Application risk score)", 71 | // "Post Bureau risk category table": "decision table(outputs : \"Post Bureau Risk Category\",input expression list : ['Existing Customer','Application Risk Score','Credit Score'],rule list : [['FALSE','< 120','<590','\"HIGH\"'],['FALSE','< 120','[590..610]','\"MEDIUM\"'],['FALSE','< 120','> 610','\"LOW\"'],['FALSE','[120..130]','<600','\"HIGH\"'],['FALSE','[120..130]','[600..625]','\"MEDIUM\"'],['FALSE','[120..130]','> 625','\"LOW\"'],['FALSE','> 130','-','\"VERY LOW\"'],['TRUE','<= 100','< 580','\"HIGH\"'],['TRUE','<= 100','[580..600]','\"MEDIUM\"'],['TRUE','<= 100','> 600','\"LOW\"'],['TRUE','> 100','< 590','\"HIGH\"'],['TRUE','> 100','[590..615]','\"MEDIUM\"'],['TRUE','> 100','> 615','\"LOW\"']],id : 'Post Bureau risk category table',hit policy: \"U\")", 72 | // "Post bureau affordability": "Affordability calculation (Monthly Income : Applicant data . Monthly . Income,Monthly Repayments : Applicant data . Monthly . Repayments,Monthly Expenses : Applicant data . Monthly . Expenses,Risk Category : Post bureau risk category,Required Monthly Installment : Required monthly installment)", 73 | // "Affordability calculation": "{Disposable Income : Monthly Income - (Monthly Repayments + Monthly Expenses),Credit Contingency Factor : Credit contingency factor,Affordability : if Disposable Income * Credit Contingency Factor > Required Monthly Installment then true else false,result : Affordability}", 74 | // "Credit contingency factor": "Credit Contingency factor table (Risk Category : Risk Category)", 75 | // "Credit Contingency factor table": "decision table(outputs : \"Credit Contingency Factor\",input expression list : ['Risk Category'],rule list : [['\"HIGH\",\"DECLINE\"','0.6'],['\"MEDIUM\"','0.7'],['\"LOW\",\"VERYLOW\"','0.8']],id : 'Credit Contingency factor table',hit policy: \"U\")", 76 | // "Required monthly installment": "Installment Calculation (Product Type : Requested product . ProductType,Rate : Requested product . Rate,Term : Requested product . Term,Amount : Requested product . Amount)", 77 | // "Installment Calculation": "{Monthly Fee : if Product Type = \"STANDARD LOAN\" then 20.00 else if Product Type = \"SPECIAL LOAN\" then 25.00 else null,Monthly Repayment : PMT(Rate, Term, Amount),result : Monthly Repayment + Monthly Fee}", 78 | // "Application risk score": "Application risk score model (Age : Applicant data . Age,Marital Status : Applicant data . MaritalStatus,Employment Status : Applicant data . EmploymentStatus)", 79 | // "Application risk score model": "decision table(outputs : \"Partial Score\",input expression list : ['Age','Marital Status','Employment Status'],rule list : [['[18..21]','-','-','32'],['[22..25]','-','-','35'],['[26..35]','-','-','40'],['[36..49]','-','-','43'],['>=50','-','-','48'],['-','\"S\"','-','25'],['-','\"M\"','-','45'],['-','-','\"UNEMPLOYED\"','15'],['-','-','\"STUDENT\"','18'],['-','-','\"EMPLOYED\"','45'],['-','-','\"SELF-EMPLOYED\"','36']],id : 'Application risk score model',hit policy: \"C+\",input values list : [[[18..120]],[\"S\", \"M\"],[\"UNEMPLOYED\", \"EMPLOYED\", \"SELF-EMPLOYED\", \"STUDENT\"]],output values : [[]])", 80 | // "PMT": "function(rate, term, amount) (amount *rate/12) / (1 - (1 + rate/12)**-term)" 81 | // }; 82 | 83 | // const decisionAST = createDecisionGraphAST(decisionMap); 84 | 85 | // executeDecisionService(decisionAST,'Routing',inputData).then((result) => { 86 | // debugger; 87 | // expect({Routing: 'ACCEPT'}).to.deep.equal(result); 88 | // done(); 89 | // }).catch(done); 90 | 91 | // }); 92 | 93 | it('should parse the RoutingRules.xlsx data without errors', function(){ 94 | var file ='test/data/RoutingRules.xlsx'; 95 | var jsonFeel = DL.parseWorkbook(file); 96 | var values = Object.keys(jsonFeel).map(k => jsonFeel[k]); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /test/decision-table/decision-table-evaluation.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var chalk = require('chalk'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DTable = require('../../utils/helper/decision-table'); 11 | var DTree = require('../../utils/helper/decision-tree'); 12 | var xlArr = ['ExamEligibility.xlsx','Adjustments.xlsx', 'Applicant_Risk_Rating.xlsx', 'ApplicantRiskRating.xlsx', 'Discount.xlsx', 'ElectricityBill.xlsx', 'Holidays.xlsx', 'PostBureauRiskCategory.xlsx', 'RoutingRules.xlsx', 'StudentFinancialPackageEligibility.xlsx', 'empty-output-check.xlsx']; 13 | var decision_table = {}; 14 | var csv = {}; 15 | var i = 0; 16 | 17 | describe(chalk.blue('Decision table evaluation'), function () { 18 | 19 | before('setup test data, read excel file and get the decision table', function (done) { 20 | done(); 21 | }); 22 | 23 | beforeEach('prepare decision table from excel and set the payload', function (done) { 24 | var path = './test/data/' + xlArr[i++]; 25 | csv = DTable.xls_to_csv(path); 26 | decision_table = DTable.csv_to_decision_table(csv[0]); 27 | done(); 28 | }); 29 | 30 | it('ExamEligibility table evaluation', function (done) { 31 | var payload = { "GPA" : 7, "d" : "1995-11-22" }; 32 | DTable.execute_decision_table("ExamEligibility", decision_table, payload, (err, results)=> { 33 | if(err){ 34 | return done(err); 35 | } 36 | expect(results.Eligible).to.be.true; 37 | done(); 38 | }); 39 | }); 40 | 41 | it('Adjustments table evaluation', function (done) { 42 | var payload = { "Customer" : "Private", "Order size" : 12 }; 43 | DTable.execute_decision_table("Adjustments", decision_table, payload, (err, results)=> { 44 | if(err){ 45 | return done(err); 46 | } 47 | expect(results.Shipping).to.equal('Air'); 48 | expect(results.Discount).to.equal(0.05); 49 | done(); 50 | }); 51 | }); 52 | 53 | it('Applicant_Risk_Rating table evaluation', function (done) { 54 | var payload = { "Applicant Age" : 25, "Medical History" : "good" }; 55 | DTable.execute_decision_table("Applicant_Risk_Rating", decision_table, payload, (err, results)=> { 56 | if(err){ 57 | return done(err); 58 | } 59 | expect(results['Applicant Risk Rating']).to.equal('Medium'); 60 | done(); 61 | }); 62 | }); 63 | 64 | it('ApplicantRiskRating table evaluation', function (done) { 65 | var payload = { "Applicant Age" : -24, "Medical History" : "bad" }; 66 | DTable.execute_decision_table("ApplicantRiskRating", decision_table, payload, (err, results)=> { 67 | if(err){ 68 | return done(err); 69 | } 70 | expect(results['Applicant Risk Rating']).to.equal('Medium'); 71 | done(); 72 | }); 73 | }); 74 | 75 | it('Discount table evaluation', function (done) { 76 | var payload = { "Customer" : "Business", "Order size" : 10 }; 77 | DTable.execute_decision_table("Discount", decision_table, payload, (err, results)=> { 78 | if(err){ 79 | return done(err); 80 | } 81 | expect(results.Discount).to.equal(0.15); 82 | done(); 83 | }); 84 | }); 85 | 86 | it('ElectricityBill table evaluation', function (done) { 87 | var payload = { "State" : "Karnataka", "Units" : 31 }; 88 | DTable.execute_decision_table("ElectricityBill", decision_table, payload, (err, results)=> { 89 | if(err){ 90 | return done(err); 91 | } 92 | expect(results.Amount).to.equal(94.4); 93 | done(); 94 | }); 95 | }); 96 | 97 | it('Holidays table evaluation', function (done) { 98 | var payload = { "Age" : 100, "Years of Service" : 200 }; 99 | DTable.execute_decision_table("Holidays", decision_table, payload, (err, results)=> { 100 | if(err){ 101 | return done(err); 102 | } 103 | expect(results.length).to.equal(5); 104 | expect(results[0].Holidays).to.equal(22); 105 | expect(results[1].Holidays).to.equal(5); 106 | expect(results[2].Holidays).to.equal(5); 107 | expect(results[3].Holidays).to.equal(3); 108 | expect(results[4].Holidays).to.equal(3); 109 | done(); 110 | }); 111 | }); 112 | 113 | it('PostBureauRiskCategory table evaluation', function (done) { 114 | var payload = {"Applicant": {"ExistingCustomer" : true}, "Report": {"CreditScore" : 600}, "b" : 60}; 115 | DTable.execute_decision_table("PostBureauRiskCategory", decision_table, payload, (err, results)=> { 116 | if(err){ 117 | return done(err); 118 | } 119 | expect(results.PostBureauRiskCategory).to.equal('MEDIUM'); 120 | done(); 121 | }); 122 | }); 123 | 124 | it('RoutingRules table evaluation', function (done) { 125 | var payload = {"Age" : 18, "Risk category" : "High", "Debt review" : false}; 126 | debugger; 127 | DTable.execute_decision_table("RoutingRules", decision_table, payload, (err, results)=> { 128 | if(err){ 129 | return done(err); 130 | } 131 | //console.log(results) 132 | expect(results.length).to.equal(2); 133 | expect(results[0].Routing).to.equal('Refer'); 134 | expect(results[0]['Review level']).to.equal('Level1'); 135 | expect(results[0].Reason).to.equal('High risk application'); 136 | expect(results[1].Routing).to.equal('Accept'); 137 | expect(results[1]['Review level']).to.equal('None'); 138 | expect(results[1].Reason).to.equal('Acceptable'); 139 | done(); 140 | }); 141 | }); 142 | 143 | it('StudentFinancialPackageEligibility table evaluation', function (done) { 144 | var payload = {"Student GPA" : 3.6,"Student Extra-Curricular Activities Count" : 4,"Student National Honor Society Membership" : "Yes"}; 145 | DTable.execute_decision_table("StudentFinancialPackageEligibility", decision_table, payload, (err, results)=> { 146 | if(err){ 147 | return done(err); 148 | } 149 | expect(results.length).to.equal(2); 150 | expect(results[0]['Student Financial Package Eligibility List']).to.equal('20% Scholarship'); 151 | expect(results[1]['Student Financial Package Eligibility List']).to.equal('30% Loan'); 152 | done(); 153 | }); 154 | }); 155 | 156 | it('empty-output-check table evaluation', function (done) { 157 | var payload = { 158 | "P": "B", 159 | "Q": 700, 160 | "R": 50, 161 | "S": 0.1, 162 | "T": "N", 163 | "U": "A" 164 | }; 165 | DTable.execute_decision_table("empty-output-check", decision_table, payload, (err, results)=> { 166 | if(err){ 167 | return done(err); 168 | } 169 | expect(results.length).to.equal(0); 170 | done(); 171 | }); 172 | }); 173 | 174 | }); 175 | -------------------------------------------------------------------------------- /test/decision-table/decision-table-to-feel-parsing.spec.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | var XLSX = require('xlsx'); 8 | var chai = require('chai'); 9 | var expect = chai.expect; 10 | var DTable = require('../../utils/helper/decision-logic'); 11 | var fs = require('fs'); 12 | 13 | var excelWorkbookPath = 'test/data/PostBureauRiskCategory2.xlsx'; 14 | 15 | describe("Internal tests...", function() { 16 | it('should be that parseXLS() returns an array', function() { 17 | 18 | var csvJson = DTable._.parseXLS(excelWorkbookPath) 19 | expect(Array.isArray(csvJson)).to.equal(true) 20 | }); 21 | 22 | it('should be that parseCsv() returns an object', function(){ 23 | var csvJson = DTable._.parseXLS(excelWorkbookPath); 24 | var resultObject = DTable._.parseCsv(csvJson); 25 | // var values = Object.values(resultObject); 26 | var values = Object.keys(resultObject).map(k => resultObject[k]); 27 | 28 | expect(Array.isArray(values)).to.equal(true); 29 | 30 | values.forEach(v => expect(v).to.be.string ); 31 | }); 32 | 33 | // it('should create the decision object without context property', function(){ 34 | // var excelSheetsCsvPartial = DTable._.parseXLS(excelWorkbookPath); 35 | // var excelSheetsJsonCsv = DTable._.parseCsv(excelSheetsCsvPartial); 36 | // var values = Object.values(excelSheetsJsonCsv); 37 | // var dto = DTable.csv_to_decision_table(values[0]); 38 | // expect(dto.context).to.be.undefined; 39 | // }); 40 | 41 | // defunct 42 | // it('should be that makeContext() returns an object', function() { 43 | // var excelSheetsCsvPartial = DTable._.parseXLS(excelWorkbookPath); 44 | // var excelSheetsJsonCsv = DTable._.parseCsv(excelSheetsCsvPartial); 45 | // var values = Object.values(excelSheetsJsonCsv); 46 | // var boxedExpression = fs.readFileSync('test\\data\\BoxedExpression-PostBureauRiskCategoryTable-Compressed.txt', { encoding: 'utf8' }); 47 | // // boxedExpression = boxedExpression.replace(/(\r\n|\n|\t)/g, '') 48 | // var dto = DTable.csv_to_decision_table(values[0]); 49 | // // debugger; 50 | // var result = DTable._.makeContext(values[0], dto) 51 | 52 | // expect(result).to.be.object 53 | 54 | // expect(Object.keys(result)).to.eql(['qn', 'expression']) 55 | // }) 56 | 57 | it('should parse decision table worksheet correctly', function() { 58 | var excelSheetsCsvPartial = DTable._.parseXLS(excelWorkbookPath); 59 | var excelSheetsJsonCsv = DTable._.parseCsv(excelSheetsCsvPartial); 60 | // var values = Object.values(excelSheetsJsonCsv); 61 | var values = Object.keys(excelSheetsJsonCsv).map(k => excelSheetsJsonCsv[k]); 62 | var decisionModelCsv = values[0]; 63 | debugger; 64 | var ctxObj = DTable._.makeContext(decisionModelCsv); 65 | var computedExpression = ctxObj.expression; 66 | 67 | //generating the expression 68 | //note: this is generated based on the worksheet 69 | //note: you'll have to manually verify this 70 | 71 | var generateContextString = DTable._.generateContextString; 72 | 73 | var contextEntries = [ 74 | { 75 | "Existing Customer" : "Applicant. ExistingCustomer", 76 | "Credit Score": "Report. CreditScore", 77 | "Application Risk Score": "Affordability Model(Applicant, Product). Application Risk Score" 78 | } 79 | ]; 80 | 81 | var ruleList = [ 82 | [ "TRUE", "<=120", "<590", '"HIGH"'], 83 | [ "TRUE", "<=120", "[590..610]", '"MEDIUM"'], 84 | [ "TRUE", "<=120", ">610", '"LOW"'], 85 | 86 | [ "FALSE", "<=100", "<580", '"HIGH"'], 87 | [ "FALSE", "<=100", "[580..600]", '"MEDIUM"'], 88 | [ "FALSE", "<=100", ">600", '"LOW"'], 89 | ]; 90 | 91 | 92 | var decisionContextEntries = [ 93 | 'outputs : "Post Bureau Risk Category"', 94 | 'input expression list : ' + generateContextString([ 95 | "Existing Customer", "Application Risk Score", "Credit Score" 96 | ], false), 97 | { 98 | "rule list" : generateContextString(ruleList.map(cl => { 99 | return generateContextString(cl, false) 100 | }), "csv"), 101 | "id" : "\'Post Bureau Risk Category Table\'" 102 | }, 103 | 'hit policy: "U"' 104 | ]; 105 | 106 | contextEntries.push({ 107 | result: `decision table(${generateContextString(decisionContextEntries, "list")})` 108 | }); 109 | 110 | var expectedExpression = generateContextString(contextEntries); 111 | // console.log('executed') 112 | // fs.writeFileSync('file1.txt', computedExpression, {encoding: 'utf8'}) 113 | // fs.writeFileSync('file2.txt', expectedExpression, {encoding: 'utf8'}) 114 | 115 | expect(computedExpression).to.equal(expectedExpression); 116 | 117 | }); 118 | 119 | it('should detect if a sheet is a decision table model', function() { 120 | var excelSheetsCsvPartial = DTable._.parseXLS(excelWorkbookPath); 121 | var excelSheetsJsonCsv = DTable._.parseCsv(excelSheetsCsvPartial); 122 | // var values = Object.values(excelSheetsJsonCsv); 123 | var values = Object.keys(excelSheetsJsonCsv).map(k => excelSheetsJsonCsv[k]); 124 | var result = DTable._.isDecisionTableModel(values[1]) 125 | 126 | expect(result).to.equal(false) 127 | 128 | result = DTable._.isDecisionTableModel(values[0]) 129 | 130 | expect(result).to.equal(true) 131 | }); 132 | 133 | it('should parse a business model worksheet correctly', function() { 134 | var excelSheetsCsvPartial = DTable._.parseXLS(excelWorkbookPath); 135 | var excelSheetsJsonCsv = DTable._.parseCsv(excelSheetsCsvPartial); 136 | // var values = Object.values(excelSheetsJsonCsv); 137 | var values = Object.keys(excelSheetsJsonCsv).map(k => excelSheetsJsonCsv[k]); 138 | var businessModelCsv = values[1]; 139 | // console.log(businessModelCsv) 140 | // debugger; 141 | var contextString = DTable._.makeContext(businessModelCsv).expression 142 | 143 | // here is the contextEntry object for the speific model 144 | //note: you'll have to manually verify this 145 | var contextEntries = [ 146 | 'Post Bureau Risk Category Table', 147 | { 148 | "Existing Customer" : "Applicant. ExistingCustomer", 149 | "Credit Score" : "Report. CreditScore", 150 | "Application Risk Score": "Affordability Model(Applicant, Product). Application Risk Score" 151 | } 152 | ]; 153 | 154 | var expected = DTable._.generateContextString(contextEntries); 155 | 156 | expect(contextString).to.equal(expected) 157 | }); 158 | 159 | it('should parse the qualified name of worksheet', function() { 160 | var excelSheetsCsvPartial = DTable._.parseXLS(excelWorkbookPath); 161 | var excelSheetsJsonCsv = DTable._.parseCsv(excelSheetsCsvPartial); 162 | // var values = Object.values(excelSheetsJsonCsv); 163 | var values = Object.keys(excelSheetsJsonCsv).map(k => excelSheetsJsonCsv[k]); 164 | var businessModelCsv = values[1]; 165 | 166 | var ctxObj = DTable._.makeContext(businessModelCsv) 167 | 168 | expect(ctxObj.qn).to.be.defined 169 | expect(ctxObj.qn).to.equal("Post Bureau Risk Category") 170 | }) 171 | }); 172 | 173 | // describe('Excel workbook parsing...', function() { 174 | // it('should parse a workbook to a json-feel boxed expression', function(){ 175 | // var jsonFeel = DTable.parseWorkbook(excelWorkbookPath); 176 | 177 | // //this jsonFeel should have two keys 178 | // var keys = Object.keys(jsonFeel) 179 | // expect(keys.length).to.equal(2) 180 | 181 | // expect(keys).to.eql(['Post Bureau Risk Category Table', 'Post Bureau Risk Category']) 182 | 183 | // //their values should be a FEEL string 184 | // keys.map(key => { 185 | // var val = jsonFeel[key] 186 | // expect(val).to.be.defined 187 | // expect(val).to.be.string 188 | // }); 189 | // }); 190 | // }); 191 | 192 | -------------------------------------------------------------------------------- /utils/helper/hit-policy.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | /* 8 | Single hit policies for single output decision tables are: 9 | 1. Unique: no overlap is possible and all rules are disjoint. Only a single rule can be matched. This is the default. 10 | 2. Any: there may be overlap, but all of the matching rules show equal output entries for each output, so any match can 11 | be used. If the output entries are non-equal, the hit policy is incorrect and the result is undefined. 12 | 3. Priority: multiple rules can match, with different output entries. This policy returns the matching rule with the 13 | highest output priority. Output priorities are specified in the ordered list of output values, in decreasing order of 14 | priority. Note that priorities are independent from rule sequence. 15 | 4. First: multiple (overlapping) rules can match, with different output entries. The first hit by rule order is returned (and 16 | evaluation can halt). This is still a common usage, because it resolves inconsistencies by forcing the first hit. 17 | However, first hit tables are not considered good practice because they do not offer a clear overview of the decision 18 | logic. It is important to distinguish this type of table from others because the meaning depends on the order of the 19 | rules. The last rule is often the catch-remainder. Because of this order, the table is hard to validate manually and 20 | therefore has to be used with care. 21 | Multiple hit policies for single output decision tables can be: 22 | 5. Output order: returns all hits in decreasing output priority order. Output priorities are specified in the ordered list of 23 | output values in decreasing order of priority. 24 | 6. Rule order: returns all hits in rule order. Note: the meaning may depend on the sequence of the rules. 25 | 7. Collect: returns all hits in arbitrary order. An operator (‘+’, ‘<’, ‘>’, ‘#’) can be added to apply a simple function to 26 | the outputs. If no operator is present, the result is the list of all the output entries. 27 | Collect operators are: 28 | a) + (sum): the result of the decision table is the sum of all the distinct outputs. 29 | b) < (min): the result of the decision table is the smallest value of all the outputs. 30 | c) > (max): the result of the decision table is the largest value of all the outputs. 31 | d) # (count): the result of the decision table is the number of distinct outputs. 32 | Other policies, such as more complex manipulations on the outputs, can be performed by post-processing the 33 | output list (outside the decision table). 34 | 35 | NOTE : Decision tables with compound outputs support only the following hit policies: Unique, Any, Priority, First, Output 36 | order, Rule order and Collect without operator, because the collect operator is undefined over multiple outputs. 37 | */ 38 | 39 | const _ = require('lodash'); 40 | 41 | const getDistinct = arr => arr.filter((item, index, arr) => arr.indexOf(item) === index); 42 | 43 | const sum = (arr) => { 44 | // const distinctArr = getDistinct(arr); 45 | const distinctArr = arr; 46 | const elem = distinctArr[0]; 47 | if (typeof elem === 'string') { 48 | return distinctArr.join(' '); 49 | } else if (typeof elem === 'number') { 50 | return distinctArr.reduce((a, b) => a + b, 0); 51 | } else if (typeof elem === 'boolean') { 52 | return distinctArr.reduce((a, b) => a && b, true); 53 | } 54 | throw new Error(`sum operation not supported for type ${typeof elem}`); 55 | }; 56 | 57 | const count = (arr) => { 58 | if (Array.isArray(arr)) { 59 | const distinctArr = getDistinct(arr); 60 | return distinctArr.length; 61 | } 62 | throw new Error(`count operation not supported for type ${typeof arr}`); 63 | }; 64 | 65 | const min = (arr) => { 66 | const elem = arr[0]; 67 | if (typeof elem === 'string') { 68 | arr.sort(); 69 | return arr[0]; 70 | } else if (typeof elem === 'number') { 71 | return Math.min(...arr); 72 | } else if (typeof elem === 'boolean') { 73 | return arr.reduce((a, b) => a && b, true) ? 1 : 0; 74 | } 75 | throw new Error(`min operation not supported for type ${typeof elem}`); 76 | }; 77 | 78 | const max = (arr) => { 79 | const elem = arr[0]; 80 | if (typeof elem === 'string') { 81 | arr.sort(); 82 | return arr[arr.length - 1]; 83 | } else if (typeof elem === 'number') { 84 | return Math.max(...arr); 85 | } else if (typeof elem === 'boolean') { 86 | return arr.reduce((a, b) => a || b, false) ? 1 : 0; 87 | } 88 | throw new Error(`max operation not supported for type ${typeof elem}`); 89 | }; 90 | 91 | const collectOperatorMap = { 92 | '+': sum, 93 | '#': count, 94 | '<': min, 95 | '>': max, 96 | }; 97 | 98 | const checkEntriesEquality = (output) => { 99 | let isEqual = true; 100 | if (output.length > 1) { 101 | const value = output[0]; 102 | output.every((other) => { 103 | isEqual = _.isEqual(value, other); 104 | return isEqual; 105 | }); 106 | return isEqual; 107 | } 108 | return isEqual; 109 | }; 110 | 111 | const getValidationErrors = output => 112 | output.filter(ruleStatus => ruleStatus.isValid === false).map((rule) => { 113 | const newRule = rule; 114 | delete newRule.isValid; 115 | return newRule; 116 | }); 117 | 118 | const hitPolicyPass = (hitPolicy, output) => new Promise((resolve, reject) => { 119 | const policy = hitPolicy.charAt(0); 120 | let ruleOutput = []; 121 | switch (policy) { 122 | // Single hit policies 123 | case 'U': 124 | ruleOutput = output.length > 1 ? {} : output[0]; 125 | break; 126 | case 'A': 127 | ruleOutput = checkEntriesEquality(output) ? output[0] : undefined; 128 | break; 129 | case 'P': 130 | ruleOutput = output[0]; 131 | break; 132 | case 'F': 133 | ruleOutput = output[0]; 134 | break; 135 | // Multiple hit policies 136 | case 'C': { 137 | const operator = hitPolicy.charAt(1); 138 | if (operator.length > 0 && output.length > 0) { 139 | const fn = collectOperatorMap[operator]; 140 | const key = Object.keys(output[0])[0]; 141 | const arr = output.map(item => item[key]); 142 | const result = {}; 143 | try { 144 | result[key] = fn(arr); 145 | } catch (e) { 146 | reject(e); 147 | } 148 | ruleOutput = result; 149 | } else { 150 | ruleOutput = output; 151 | } 152 | break; 153 | } 154 | case 'R': 155 | ruleOutput = output; 156 | break; 157 | case 'O': 158 | ruleOutput = output; 159 | break; 160 | case 'V': 161 | ruleOutput = getValidationErrors(output); 162 | break; 163 | default : 164 | ruleOutput = output; 165 | } 166 | resolve(ruleOutput); 167 | }); 168 | 169 | const prepareOutputOrder = (output, priorityList) => { 170 | const arr = output.map((rule) => { 171 | const obj = {}; 172 | obj.rule = rule; 173 | obj.priority = priorityList[rule]; 174 | return obj; 175 | }); 176 | const sortedPriorityList = _.sortBy(arr, ['priority']); 177 | const outputList = sortedPriorityList.map(ruleObj => ruleObj.rule); 178 | return outputList; 179 | }; 180 | 181 | const ruleSorter = function (a, b) { 182 | const left = parseInt(a.substr(4), 10); 183 | const right = parseInt(b.substr(4), 10); 184 | if (left < right) { 185 | return -1; 186 | } else if (left > right) { 187 | return 1; 188 | } 189 | return 0; 190 | }; 191 | 192 | const getOrderedOutput = (root, outputList) => { 193 | const policy = root.hitPolicy.charAt(0); 194 | let outputOrderedList = []; 195 | switch (policy) { 196 | case 'P': 197 | outputOrderedList.push(prepareOutputOrder(outputList, root.priorityList)[0]); 198 | break; 199 | case 'O': 200 | outputOrderedList = prepareOutputOrder(outputList, root.priorityList); 201 | break; 202 | case 'F': 203 | outputOrderedList = outputList.sort(ruleSorter).slice(0, 1); 204 | break; 205 | case 'R': 206 | outputOrderedList = outputList.sort(ruleSorter); 207 | break; 208 | default : 209 | outputOrderedList = outputList; 210 | } 211 | return outputOrderedList; 212 | }; 213 | 214 | module.exports = { hitPolicyPass, getOrderedOutput }; 215 | -------------------------------------------------------------------------------- /utils/helper/decision-tree.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ©2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | const _ = require('lodash'); 9 | const FEEL = require('../../dist/feel').parse; 10 | const { getOrderedOutput, hitPolicyPass } = require('./hit-policy.js'); 11 | 12 | function Node(data, type) { 13 | this.data = data; 14 | this.type = type; 15 | this.children = {}; 16 | } 17 | 18 | function Tree(data) { 19 | const node = new Node(data, 'Root'); 20 | this.root = node; 21 | } 22 | 23 | function generatePriorityList(dTable) { 24 | const outputs = dTable.outputs && Array.isArray(dTable.outputs) ? dTable.outputs : [dTable.outputs]; 25 | const outputValuesList = dTable.outputValues; 26 | // .map(outputValue => FEEL(outputValue)); 27 | const ruleList = dTable.ruleList; 28 | const numOfConditions = dTable.inputExpressionList.length; 29 | const calcPriority = (priorityMat, outputs) => { 30 | const sortedPriority = _.sortBy(priorityMat, outputs); 31 | const rulePriority = {}; 32 | sortedPriority.forEach((priority, index) => { 33 | const key = `Rule${priority.Rule}`; 34 | rulePriority[key] = index + 1; 35 | }); 36 | return rulePriority; 37 | }; 38 | 39 | const matrix = []; 40 | ruleList.forEach((ruleLine, ruleIndex) => { 41 | ruleLine.forEach((ruleComponent, ordinal) => { 42 | const pty = matrix[ruleIndex] || {}; 43 | pty.Rule = ruleIndex + 1; 44 | const outputOrdinal = ordinal - numOfConditions; 45 | if (outputOrdinal < 0) { return; } 46 | const pValue = outputOrdinal < 0 ? -1 : outputValuesList[outputOrdinal].indexOf(ruleComponent); 47 | pty[outputs[outputOrdinal]] = (pValue === -1 ? 0 : (pValue + 1)); 48 | matrix[ruleIndex] = pty; 49 | }); 50 | }); 51 | 52 | return calcPriority(matrix, outputs); 53 | } 54 | 55 | 56 | const createDecisionTree = (dTable) => { 57 | const ruleTree = new Tree('Rule'); 58 | const root = ruleTree.root; 59 | const classNodeList = dTable.inputExpressionList; 60 | const numOfConditions = classNodeList.length; 61 | const outputNodeList = dTable.outputs && Array.isArray(dTable.outputs) ? dTable.outputs : [dTable.outputs]; 62 | const ruleList = dTable.ruleList; 63 | const outputSet = {}; 64 | 65 | root.hitPolicy = dTable.hitPolicy; 66 | if (root.hitPolicy === 'P' || root.hitPolicy === 'O') { 67 | if (dTable.priorityList) { 68 | root.priorityList = dTable.priorityList; 69 | } else { 70 | root.priorityList = generatePriorityList(dTable); 71 | } 72 | } 73 | 74 | if (dTable.context !== null) { 75 | root.context = new Node(dTable.context, 'Context'); 76 | root.context.ast = FEEL(root.context.data); 77 | root.context.children = null; 78 | } else { 79 | root.context = dTable.context; 80 | } 81 | classNodeList.forEach((classValue) => { 82 | const node = new Node(classValue, 'Class'); 83 | node.ast = null; 84 | root.children[classValue] = node; 85 | }); 86 | 87 | ruleList.forEach((row, rowIndex) => { 88 | const outputValue = {}; 89 | const index = rowIndex + 1; 90 | const data = `Rule${index}`; 91 | const sentinelNode = new Node(data, 'Sentinel'); 92 | sentinelNode.children = null; 93 | row.forEach((cellValue, colIndex) => { 94 | if (colIndex < numOfConditions) { 95 | const node = root.children[classNodeList[colIndex]].children[cellValue] || new Node(cellValue, 'Value'); 96 | node.ast = node.ast || FEEL(cellValue, { startRule: 'SimpleUnaryTests' }); 97 | node.children[data] = sentinelNode; 98 | root.children[classNodeList[colIndex]].children[cellValue] = root.children[classNodeList[colIndex]].children[cellValue] || node; 99 | } else { 100 | const node = new Node(cellValue); 101 | node.ast = FEEL(cellValue); 102 | node.children = null; 103 | outputValue[outputNodeList[colIndex - numOfConditions]] = node; 104 | } 105 | }); 106 | outputSet[data] = outputValue; 107 | }); 108 | 109 | root.outputSet = outputSet; 110 | return root; 111 | }; 112 | 113 | const prepareOutput = (outputSet, output, payload) => 114 | new Promise((resolve, reject) => { 115 | Promise.all(output.map((i) => { 116 | const keys = Object.keys(outputSet[i]); 117 | return new Promise((resolve, reject) => { // eslint-disable-line 118 | Promise.all(keys.map(k => outputSet[i][k].ast.build(payload))).then((results) => { 119 | resolve(results.reduce((res, val, j) => { 120 | const obj = {}; 121 | obj[keys[j]] = val; 122 | return Object.assign({}, obj, res); 123 | }, {})); 124 | }).catch(err => reject(err)); 125 | }); 126 | })).then(results => 127 | resolve(results)).catch(err => reject(err)); 128 | }); 129 | 130 | const resolveConflictRules = (root, payload, rules) => { 131 | let output = []; 132 | const rootChildren = root.children; 133 | const classArr = Object.keys(rootChildren); 134 | 135 | classArr.every((classNode, i) => { 136 | const valueKeys = Object.keys(rootChildren[classNode].children); 137 | const matchKeys = rules[i]; 138 | let arr = []; 139 | if (matchKeys.length === 0) { 140 | output = []; 141 | return false; 142 | } 143 | valueKeys.forEach((valKey, j) => { 144 | if (matchKeys.indexOf(j) > -1) { 145 | arr = arr.concat(Object.keys(rootChildren[classNode].children[valKey].children)); 146 | } 147 | }); 148 | 149 | arr = arr.filter((item, index) => arr.indexOf(item) === index); 150 | if (i === 0) { 151 | output = arr; 152 | } else if (output.length > 0) { 153 | output = arr.filter(d => output.indexOf(d) > -1); 154 | } else { 155 | return false; 156 | } 157 | 158 | // output = output.length > 0 ? arr.filter(d => output.indexOf(d) > -1) : arr; 159 | 160 | return true; 161 | }); 162 | 163 | if (output.length > 0) { 164 | return prepareOutput(root.outputSet, getOrderedOutput(root, output), payload); 165 | } 166 | return Promise.resolve([]); 167 | }; 168 | 169 | const traverseDecisionTreeUtil = (root, payload) => { 170 | const classArr = Object.keys(root.children); 171 | return new Promise((resolve, reject) => { 172 | Promise.all(classArr.map((classKey) => { 173 | const node = root.children[classKey]; 174 | const sentinelKeys = Object.keys(node.children); 175 | const { decisionMap } = payload; 176 | 177 | return new Promise((resolve, reject) => { 178 | const inputExpressionValue = payload[classKey]; 179 | if (typeof inputExpressionValue !== 'undefined') { 180 | //! the input expression is resolved from payload 181 | Promise.all(sentinelKeys.map(key => node.children[key].ast.build(payload, {}, 'input'))).then((results) => { 182 | let res = results.map((f, i) => ({ value: f(inputExpressionValue), index: i })).filter(d => d.value === true); 183 | res = res.map(obj => obj.index); 184 | resolve(res); 185 | }).catch(err => reject(err)); 186 | } else if (decisionMap[classKey]) { 187 | //! the input expression can be resolved from the decisionMap 188 | const decision = decisionMap[classKey]; 189 | decision.build(payload) 190 | .then((value) => { 191 | Promise.all(sentinelKeys.map(key => node.children[key].ast.build(payload, {}, 'input'))) 192 | .then((results) => { 193 | let res = results.map((f, i) => ({ value, index: i })) 194 | .filter(d => d.value); 195 | res = res.map(obj => obj.index); 196 | resolve(res); 197 | }) 198 | .catch(reject); 199 | }) 200 | .catch(reject); 201 | } else { 202 | reject(new Error(`Cannot resolve decision table input expression: ${classKey}`)); 203 | } 204 | }); 205 | })).then(results => 206 | resolveConflictRules(root, payload, results).then(results => 207 | resolve(results))).catch(err => reject(err)); 208 | }); 209 | }; 210 | 211 | const prepareContext = (root, payload) => { 212 | if (root.context !== null) { 213 | return root.context.ast.build(payload); 214 | } 215 | return Promise.resolve(payload); 216 | }; 217 | 218 | const traverseDecisionTree = (root, payload) => new Promise((resolve, reject) => { 219 | prepareContext(root, payload) 220 | .then((context) => { 221 | const ctx = Object.assign({}, payload, context); 222 | return traverseDecisionTreeUtil(root, ctx); 223 | }) 224 | .then(results => hitPolicyPass(root.hitPolicy, results)) 225 | .then(output => resolve(output)) 226 | .catch(err => reject(err)); 227 | }); 228 | 229 | module.exports = { 230 | createTree: createDecisionTree, 231 | traverseTree: traverseDecisionTree, 232 | }; 233 | -------------------------------------------------------------------------------- /utils/helper/decision-table.js: -------------------------------------------------------------------------------- 1 | /* 2 | * 3 | * ??2016-2017 EdgeVerve Systems Limited (a fully owned Infosys subsidiary), 4 | * Bangalore, India. All Rights Reserved. 5 | * 6 | */ 7 | 8 | 9 | const XLSX = require('xlsx'); 10 | const _ = require('lodash'); 11 | const tree = require('./decision-tree.js'); 12 | 13 | const rootMap = {}; 14 | const delimiter = '&SP'; 15 | 16 | const parseXLS = (path) => { 17 | const workbook = XLSX.readFile(path); 18 | const csv = []; 19 | workbook.SheetNames.forEach((sheetName) => { 20 | /* iterate through sheets */ 21 | const worksheet = workbook.Sheets[sheetName]; 22 | csv.push(XLSX.utils.sheet_to_csv(worksheet, { FS: delimiter })); 23 | }); 24 | 25 | return csv; 26 | }; 27 | 28 | const getFormattedValue = str => str.replace(/\"{2,}/g, '\"').replace(/^\"|\"$/g, ''); 29 | 30 | const parseContext = (csv) => { 31 | let context = {}; 32 | let i = 1; 33 | 34 | for (; i < csv.length; i += 1) { 35 | const arr = csv[i].split(delimiter).filter(String); 36 | if (arr.length > 0 && arr[0] === 'RuleTable') { 37 | break; 38 | } else if (arr.length > 0) { 39 | const count = arr[1].split('"').length - 1; 40 | if (count > 0) { 41 | arr[1] = getFormattedValue(arr[1]); 42 | } 43 | context[arr[0]] = arr[1]; 44 | } 45 | } 46 | context = Object.keys(context).length > 0 ? JSON.stringify(context).replace(/"/g, '').replace(/\\/g, '"') : ''; 47 | return context.length > 0 ? context : null; 48 | }; 49 | 50 | const preparePriorityList = (priorityClass, priorityValues) => { 51 | const priority = {}; 52 | priorityClass.forEach((pClass, index) => { 53 | priority[pClass] = priority[pClass] || {}; 54 | let p = 1; 55 | priorityValues[index].forEach((value) => { 56 | priority[pClass][value] = p; 57 | p += 1; 58 | }); 59 | }); 60 | return priority; 61 | }; 62 | 63 | const calculatePriority = (priorityMat, outputs) => { 64 | const sortedPriority = _.sortBy(priorityMat, outputs); 65 | const rulePriority = {}; 66 | sortedPriority.forEach((priority, index) => { 67 | const key = `Rule${priority.Rule}`; 68 | rulePriority[key] = index + 1; 69 | }); 70 | return rulePriority; 71 | }; 72 | 73 | const createDecisionTable = (commaSeparatedValue) => { 74 | const decisionTable = {}; 75 | decisionTable.inputExpressionList = []; 76 | decisionTable.inputValuesList = []; 77 | decisionTable.outputs = []; 78 | decisionTable.outputValues = []; 79 | decisionTable.ruleList = []; 80 | 81 | const inputExpressionList = []; 82 | let outputs = []; 83 | const inputValuesSet = {}; 84 | const outputValuesList = []; 85 | let outputLabel = false; 86 | let priority = {}; 87 | const priorityMat = []; 88 | 89 | const csv = commaSeparatedValue.split('\n'); 90 | 91 | let numOfConditions = 0; 92 | let numOfActions = 0; 93 | let i = 0; 94 | const conditionActionFilter = (elem) => { 95 | if (elem === 'Condition') { 96 | numOfConditions += 1; 97 | } else if (elem === 'Action') { 98 | numOfActions += 1; 99 | } 100 | }; 101 | 102 | for (;i < csv.length; i += 1) { 103 | const arr = csv[i].split(delimiter); 104 | if (arr[0] === 'RuleTable') { 105 | arr.forEach(conditionActionFilter); 106 | break; 107 | } 108 | } 109 | 110 | i += 1; 111 | const classArr = csv[i].split(delimiter); 112 | decisionTable.hitPolicy = classArr[0]; 113 | 114 | // input and output classes 115 | classArr.slice(1).every((classValue, index) => { 116 | if (index < numOfConditions) { 117 | inputExpressionList.push(classValue); 118 | } else { 119 | if (classValue === '') { 120 | outputLabel = true; 121 | return false; 122 | } 123 | outputs.push(classValue); 124 | } 125 | return true; 126 | }); 127 | i += 1; 128 | let values = csv[i].split(/&SP(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/); 129 | // if there is a output label which contains the output component names 130 | if (outputLabel) { 131 | outputs = []; 132 | numOfActions = 0; 133 | values = values.filter(String);// removes all blank strings from array 134 | values.forEach((action) => { 135 | numOfActions += 1; 136 | outputs.push(action); 137 | }); 138 | i += 1; 139 | values = csv[i].split(/&SP(?![^"]*"(?:(?:[^"]*"){2})*[^"]*$)/); 140 | } 141 | // "Collect" Hit Policy Check 142 | if (decisionTable.hitPolicy && decisionTable.hitPolicy.charAt(0) === 'C' && decisionTable.hitPolicy.charAt(1) !== '' && numOfActions > 1) { 143 | throw new Error({ 144 | hitPolicy: decisionTable.hitPolicy, 145 | actionItems: numOfActions, 146 | message: 'Hit policy violation, collect operator is undefined over multiple outputs', 147 | }); 148 | } 149 | 150 | // input and output values 151 | if (values[0] === '') { 152 | values.slice(1).forEach((classValue, index) => { 153 | let value = classValue; 154 | value = value.replace(/(^")|("$)/g, ''); 155 | if (index < numOfConditions) { 156 | inputValuesSet[inputExpressionList[index]] = value.split(',').filter(String).map((inVal) => { 157 | let val = inVal; 158 | if (val.split('"').length - 1 > 0) { 159 | val = val.replace(/""/g, '\"'); 160 | } 161 | return val; 162 | }); 163 | } else { 164 | outputValuesList[index - numOfConditions] = []; 165 | outputValuesList[index - numOfConditions] = value.split(',').filter(String).map((outVal) => { 166 | let val = outVal; 167 | if (val.split('"').length - 1 > 0) { 168 | val = val.replace(/""/g, '\"'); 169 | } 170 | return val; 171 | }); 172 | } 173 | }); 174 | if (decisionTable.hitPolicy === 'P' || decisionTable.hitPolicy === 'O') { 175 | priority = preparePriorityList(outputs, outputValuesList); 176 | } 177 | i += 1; 178 | } else { 179 | inputExpressionList.forEach((condition) => { 180 | inputValuesSet[condition] = []; 181 | }); 182 | outputs.forEach((action, i) => { 183 | outputValuesList[i] = []; 184 | }); 185 | } 186 | 187 | // rulelist 188 | let prevRuleRow = []; 189 | 190 | const processCellValue = (value, index) => { 191 | let cellValue = value; 192 | if (cellValue === '') { 193 | cellValue = prevRuleRow[index]; 194 | } else { 195 | const count = cellValue.split('"').length - 1; 196 | if (count > 0) { 197 | cellValue = cellValue.replace(/""/g, '\"').replace(/^\"|\"$/g, ''); 198 | } 199 | if ((decisionTable.hitPolicy === 'P' || decisionTable.hitPolicy === 'O') && (index >= numOfConditions)) { 200 | priorityMat[decisionTable.ruleList.length] = priorityMat[decisionTable.ruleList.length] || {}; 201 | priorityMat[decisionTable.ruleList.length].Rule = decisionTable.ruleList.length + 1; 202 | priorityMat[decisionTable.ruleList.length][outputs[index - numOfConditions]] = priority[outputs[index - numOfConditions]][cellValue] || 0; 203 | } 204 | if (index < numOfConditions && inputValuesSet[inputExpressionList[index]].indexOf(cellValue) === -1) { 205 | inputValuesSet[inputExpressionList[index]].push(cellValue); 206 | } else if (index >= numOfConditions && outputValuesList[index - numOfConditions].indexOf(cellValue) === -1) { 207 | outputValuesList[index - numOfConditions].push(cellValue); 208 | // problem in xls 0.10 is treated as 0.1 but string treats 0.10 as 0.10 209 | } 210 | } 211 | return cellValue; 212 | }; 213 | 214 | for (; i < csv.length; i += 1) { 215 | let currentRuleRow = csv[i].split(delimiter).slice(1); 216 | currentRuleRow = currentRuleRow.map(processCellValue); 217 | if (currentRuleRow.length > 0) { 218 | decisionTable.ruleList.push(currentRuleRow); 219 | prevRuleRow = currentRuleRow; 220 | } 221 | } 222 | 223 | if (priorityMat.length > 0) { 224 | decisionTable.priorityList = calculatePriority(priorityMat, outputs); 225 | } 226 | 227 | Object.keys(inputValuesSet).forEach((classKey) => { 228 | decisionTable.inputValuesList.push(inputValuesSet[classKey].toString()); 229 | }); 230 | decisionTable.inputExpressionList = inputExpressionList; 231 | decisionTable.outputs = outputs; 232 | decisionTable.outputValues = outputValuesList; 233 | decisionTable.context = parseContext(csv); 234 | 235 | return decisionTable; 236 | }; 237 | 238 | // function updateDecisionTable(id, csv) { 239 | // let table = {}; 240 | 241 | // table = createDecisionTable(csv); 242 | // rootMap[id] = tree.createTree(table); 243 | 244 | // return table; 245 | // } 246 | 247 | const executeDecisionTable = (id, table, payload, cb) => { 248 | const graphName = payload.graphName; 249 | let rootMapId = id; 250 | if (graphName) { 251 | rootMapId = `${graphName}${id}`; 252 | } 253 | if (rootMap[rootMapId] == null || rootMap[rootMapId] === 'undefined') { 254 | try { 255 | rootMap[rootMapId] = tree.createTree(table); 256 | } catch (e) { 257 | cb(e); 258 | return; 259 | } 260 | } 261 | tree.traverseTree(rootMap[rootMapId], payload) 262 | .then(result => cb(null, result)) 263 | .catch(err => cb(err)); 264 | }; 265 | 266 | module.exports = { 267 | csv_to_decision_table: createDecisionTable, 268 | xls_to_csv: parseXLS, 269 | execute_decision_table: executeDecisionTable, 270 | }; 271 | --------------------------------------------------------------------------------