├── .editorconfig ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .gitignore ├── .gitlab-ci.yml ├── .npmignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── feel-ast-parser.js ├── feel-ast.js └── feel.js ├── dmn-feel-grammar.txt ├── dmn-input-variables.png ├── dmn-output.png ├── grammar ├── feel-initializer.js └── feel.pegjs ├── gulpfile.js ├── index.js ├── package-lock.json ├── package.json ├── src ├── feel-ast-parser.js ├── feel-ast.js └── feel.pegjs ├── test ├── arithmetic-expression │ ├── date-time-duration-arithmetic-expression.build.spec.js │ ├── feel-arithmetic-expression.build.spec.js │ └── feel-arithmetic-expression.parse.spec.js ├── ast-parser-test.spec.js ├── comparision-expression │ └── feel-comparision-expression.build.spec.js ├── data │ ├── test-collect-drg.dmn │ ├── test-collect.dmn │ ├── test-decode-special-characters.dmn │ ├── test-empty-decision.dmn │ ├── test-empty-input.dmn │ ├── test-input-expression.dmn │ ├── test-input-variable.dmn │ ├── test-no-matching-rule.dmn │ ├── test-no-matching-rules.dmn │ ├── test-rule-order.dmn │ ├── test-type-error.dmn │ ├── test-undefined-input-entry.dmn │ ├── test-undefined-output-expression-collect.dmn │ ├── test-undefined-output-expression.dmn │ ├── test-unique.dmn │ └── test.dmn ├── decision-table-xml.spec.js ├── feel-miscellaneous-expression.build.spec.js ├── handle-undefined-values.spec.js ├── list-functions.build.spec.js └── string-functions.build.spec.js └── utils ├── built-in-functions ├── boolean-functions │ ├── index.js │ └── not.js ├── date-time-functions │ ├── add-properties.js │ ├── date-time.js │ ├── date.js │ ├── duration.js │ ├── index.js │ ├── misc.js │ └── time.js ├── defined.js ├── index.js ├── list-functions │ └── index.js └── string-functions │ ├── contains.js │ ├── ends-with.js │ ├── index.js │ ├── lower-case.js │ ├── starts-with.js │ └── upper-case.js ├── dev └── gulp-pegjs.js └── helper ├── add-kwargs.js ├── decision-table-xml.js ├── external-function.js ├── fn-generator.js ├── hit-policy.js ├── meta.js ├── name-resolution.js └── value.js /.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 -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /grammar/* 2 | /test/* 3 | /dist/* 4 | gulpfile.js 5 | /_archive/* 6 | /utils/dev/* 7 | dummy.js 8 | -------------------------------------------------------------------------------- /.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 | "no-underscore-dangle":"off" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.js text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dummy.js 3 | npm-debug.log 4 | *.iml 5 | /.idea 6 | 7 | # webpack and babel 8 | /dist/dmn-eval-js.js 9 | webpack.config.js 10 | .babelrc 11 | 12 | # Coveralls 13 | coverage 14 | 15 | # GitLab 16 | .gitlab-ci.yml 17 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | build_job: 2 | script: 3 | - evf-paas login -u admin -p Anything_f9 evfapp.dev -d && source $HOME/evf-paas/evfapp.dev/install_env.sh 4 | - npm config set registry $NPM_REGISTRY 5 | - npm config set fetch-retries 0 6 | - npm config set loglevel warn 7 | - time npm install --no-optional 8 | - gulp lint 9 | - export exit_status=0 10 | - if gulp test-ci-html; then exit_status=0; else exit_status=1; fi 11 | - if echo $CI_BUILD_REPO | grep "oecloud.io/feel.git"; then export MAIN_BRANCH=1; else export MAIN_BRANCH=0; fi 12 | - export MAIN_BRANCH=1 13 | - if [ $MAIN_BRANCH -eq 0 ]; then echo "Not the main project, hence not sending coverage report to evgit..."; exit $exit_status; fi 14 | - echo "Sending coverage reports to evgit..." 15 | - tar -cf feel.tar -C ./coverage/lcov-report/ . 16 | - ssh root@10.73.53.167 'rm -rf /data/documentation-portal/coverage/feel' 17 | - scp feel.tar root@10.73.53.167:/data/documentation-portal/coverage/ 18 | - ssh root@10.73.53.167 'mkdir -p /data/documentation-portal/coverage/feel' 19 | - ssh root@10.73.53.167 'tar -xf /data/documentation-portal/coverage/feel.tar -C /data/documentation-portal/coverage/feel' 20 | - ssh root@10.73.53.167 'rm -rf /data/documentation-portal/coverage/feel.tar' 21 | - echo "Coverage report updated" 22 | - exit $exit_status 23 | tags: 24 | - EVF-PAAS -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | #tests 2 | test 3 | coverage 4 | src 5 | grammar 6 | 7 | #build tools 8 | .travis.yml 9 | gulpfile.js 10 | .idea 11 | 12 | #linters 13 | .eslintrc 14 | .eslintignore 15 | 16 | #editor settings 17 | .editorconfig 18 | 19 | #markdown 20 | CONTRIBUTION.md 21 | CORPORATE_CLA.md 22 | INDIVIDUAL_CLA.md 23 | 24 | #textfile 25 | dmn-feel-grammar.txt 26 | 27 | #git 28 | .gitattributes 29 | .gitignore 30 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "6" 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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | This software uses substantial portions of the software FEEL by EdgeVerve. Copyright (c) 2016-2017 EdgeVerve 4 | 5 | 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: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | 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. -------------------------------------------------------------------------------- /dist/feel-ast-parser.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 _ = require('lodash'); 10 | const fnGen = require('../utils/helper/fn-generator'); 11 | const addKwargs = require('../utils/helper/add-kwargs'); 12 | const builtInFns = require('../utils/built-in-functions'); 13 | const resolveName = require('../utils/helper/name-resolution.js'); 14 | const logger = require('loglevel').getLogger('dmn-eval-js'); 15 | 16 | module.exports = function (ast) { 17 | ast.ProgramNode.prototype.build = function (data = {}, env = {}, type = 'output') { 18 | let args = {}; 19 | if (!data.isContextBuilt) { 20 | const context = Object.assign({}, data, builtInFns); 21 | args = Object.assign({}, { context }, env); 22 | args.isContextBuilt = true; 23 | } else { 24 | args = data; 25 | } 26 | // bodybuilding starts here... 27 | // let's pump some code ;) 28 | const result = this.body.build(args); 29 | if (type === 'input') { 30 | if (typeof result === 'function') { 31 | return result; 32 | } 33 | const fnResult = function (x) { 34 | return x === result; 35 | }; 36 | return fnResult; 37 | } 38 | return result; 39 | }; 40 | 41 | ast.IntervalStartLiteralNode.prototype.build = function () { 42 | return fnGen(this.intervalType); 43 | }; 44 | 45 | ast.IntervalEndLiteralNode.prototype.build = function () { 46 | return fnGen(this.intervalType); 47 | }; 48 | 49 | ast.IntervalNode.prototype.build = function (args) { 50 | const startpoint = this.startpoint.build(args); 51 | const endpoint = this.endpoint.build(args); 52 | return (x) => { 53 | const startValue = this.intervalstart.build()(startpoint)(x); 54 | const endValue = this.intervalend.build()(endpoint)(x); 55 | return startValue === undefined || endValue === undefined ? undefined : startValue && endValue; 56 | }; 57 | }; 58 | 59 | ast.SimplePositiveUnaryTestNode.prototype.build = function (args) { 60 | const result = this.operand.build(args); 61 | 62 | // hack to treat input expressions as input variables and let functions in input entries reference them 63 | // for example: starts with(name, prefix) 64 | // where "name" is the input expression 65 | // for this to work, if the result of the function is true (like in the example above), that value cannot be 66 | // compared with the the evaluated input expression (which is the value of the input variable), so we must 67 | // patch the comparison here 68 | if (args.context._inputVariableName && this.operand.type === 'FunctionInvocation' && this.operand.params) { 69 | // patch only if there is an input variable and the simple positive unary test contains a function directly, 70 | // where the input variable in a parameter of that function 71 | const nodeIsQualifiedNameOfInputVariable = node => 72 | node.type === 'QualifiedName' && node.names.map(nameNode => nameNode.nameChars).join('.') === args.context._inputVariableName; 73 | const inputVariableParameter = (this.operand.params.params || []).find(node => nodeIsQualifiedNameOfInputVariable(node)); 74 | if (inputVariableParameter) { 75 | if (result === true) { 76 | // if the function evaluates to true, compare the evaluated input expression with the evaluated input variable, 77 | // not with the result of the function evaluation 78 | return fnGen(this.operator || '==')(_, inputVariableParameter.build(args)); 79 | } else if (result === false) { 80 | // if the function evaluates to false, the simple positive unary test should always evaluate to false 81 | return () => false; 82 | } 83 | } 84 | } 85 | 86 | return fnGen(this.operator || '==')(_, result); 87 | }; 88 | 89 | ast.SimpleUnaryTestsNode.prototype.build = function (data = {}) { 90 | const context = Object.assign({}, data, builtInFns); 91 | const args = { context }; 92 | if (this.expr) { 93 | const results = this.expr.map(d => d.build(args)); 94 | if (this.not) { 95 | const negResults = results.map(result => args.context.not(result)); 96 | return x => negResults.reduce((result, next) => { 97 | const nextValue = next(x); 98 | return (result === false || nextValue === false) ? false : ((result === undefined || nextValue === undefined) ? undefined : (result && nextValue)); 99 | }, true); 100 | } 101 | return x => results.reduce((result, next) => { 102 | const nextValue = next(x); 103 | return (result === true || nextValue === true) ? true : ((result === undefined || nextValue === undefined) ? undefined : (result || nextValue)); 104 | }, false); 105 | } 106 | return () => true; 107 | }; 108 | 109 | ast.QualifiedNameNode.prototype.build = function (args, doNotWarnIfUndefined = false) { 110 | const [first, ...remaining] = this.names; 111 | const buildNameNode = (name) => { 112 | const result = { nameNode: name, value: name.build(null, false) }; 113 | return result; 114 | }; 115 | const processRemaining = (firstResult, firstExpression) => remaining.map(buildNameNode) 116 | .reduce((prev, next) => { 117 | if (prev.value === undefined) { 118 | return prev; 119 | } 120 | return { value: prev.value[next.value], expression: `${prev.expression}.${next.nameNode.nameChars}` }; 121 | }, { value: firstResult, expression: firstExpression }); 122 | 123 | const firstResult = first.build(args); 124 | if (remaining.length) { 125 | const fullResult = processRemaining(firstResult, first.nameChars); 126 | if (fullResult.value === undefined) { 127 | if (!doNotWarnIfUndefined) { 128 | logger.info(`'${fullResult.expression}' resolved to undefined`); 129 | } 130 | } 131 | return fullResult.value; 132 | } 133 | if (firstResult === undefined) { 134 | if (!doNotWarnIfUndefined) { 135 | logger.info(`'${first.nameChars}' resolved to undefined`); 136 | } 137 | } 138 | return firstResult; 139 | }; 140 | 141 | ast.ArithmeticExpressionNode.prototype.build = function (args) { 142 | const operandsResult = [this.operand_1, this.operand_2].map((op) => { 143 | if (op === null) { 144 | return 0; 145 | } 146 | return op.build(args); 147 | }); 148 | return fnGen(this.operator)(operandsResult[0])(operandsResult[1]); 149 | }; 150 | 151 | ast.SimpleExpressionsNode.prototype.build = function (data = {}, env = {}) { 152 | let context = {}; 153 | if (!data.isBuiltInFn) { 154 | context = Object.assign({}, data, builtInFns, { isBuiltInFn: true }); 155 | } else { 156 | context = data; 157 | } 158 | const args = Object.assign({}, { context }, env); 159 | return this.simpleExpressions.map(d => d.build(args)); 160 | }; 161 | 162 | // _fetch is used to return the name string or 163 | // the value extracted from context or kwargs using the name string 164 | ast.NameNode.prototype.build = function (args, _fetch = true) { 165 | const name = this.nameChars; 166 | if (!_fetch) { 167 | return name; 168 | } 169 | 170 | return resolveName(name, args); 171 | }; 172 | 173 | ast.LiteralNode.prototype.build = function () { 174 | return this.value; 175 | }; 176 | 177 | ast.DateTimeLiteralNode.prototype.build = function (args) { 178 | const fn = args.context[this.symbol]; 179 | const paramsResult = this.params.map(d => d.build(args)); 180 | let result; 181 | if (!paramsResult.includes(undefined)) { 182 | result = fn(...paramsResult); 183 | } 184 | return result; 185 | }; 186 | 187 | // Invoking function defined as boxed expression in the context entry 188 | // See ast.FunctionDefinitionNode for details on declaring function 189 | // Function supports positional as well as named parameters 190 | ast.FunctionInvocationNode.prototype.build = function (args) { 191 | const processFormalParameters = (formalParams) => { 192 | const values = this.params.build(args); 193 | if (formalParams && values && Array.isArray(values)) { 194 | const kwParams = values.reduce((recur, next, i) => { 195 | const obj = {}; 196 | obj[formalParams[i]] = next; 197 | return Object.assign({}, recur, obj); 198 | }, {}); 199 | return addKwargs(args, kwParams); 200 | } 201 | return addKwargs(args, values); 202 | }; 203 | 204 | const processUserDefinedFunction = (fnMeta) => { 205 | const fn = fnMeta.fn; 206 | const formalParams = fnMeta.params; 207 | 208 | if (formalParams) { 209 | return fn.build(processFormalParameters(formalParams)); 210 | } 211 | return fn.build(args); 212 | }; 213 | 214 | const processInBuiltFunction = (fnMeta) => { 215 | const doNotWarnIfUndefined = fnMeta.name === 'defined'; 216 | const values = this.params.build(args, doNotWarnIfUndefined); 217 | if (Array.isArray(values)) { 218 | return fnMeta(...[...values, args.context]); 219 | } 220 | return fnMeta(Object.assign({}, args.context, args.kwargs), values); 221 | }; 222 | 223 | const processDecision = (fnMeta) => { 224 | const expr = fnMeta.expr; 225 | if (expr.body instanceof ast.FunctionDefinitionNode) { 226 | const exprResult = expr.body.build(args); 227 | return processUserDefinedFunction(exprResult); 228 | } 229 | const formalParametersResult = processFormalParameters(); 230 | return expr.build(formalParametersResult); 231 | }; 232 | 233 | const processFnMeta = (fnMeta) => { 234 | if (typeof fnMeta === 'function') { 235 | return processInBuiltFunction(fnMeta); 236 | } else if (typeof fnMeta === 'object' && fnMeta.isDecision) { 237 | return processDecision(fnMeta); 238 | } 239 | return processUserDefinedFunction(fnMeta); 240 | }; 241 | 242 | const fnNameResult = this.fnName.build(args); 243 | let result; 244 | if (fnNameResult !== undefined) { 245 | result = processFnMeta(fnNameResult); 246 | } 247 | return result; 248 | }; 249 | 250 | ast.PositionalParametersNode.prototype.build = function (args, doNotWarnIfUndefined = false) { 251 | const results = this.params.map(d => d.build(args, doNotWarnIfUndefined)); 252 | return results; 253 | }; 254 | 255 | ast.ComparisonExpressionNode.prototype.build = function (args) { 256 | let operator = this.operator; 257 | if (operator === 'between') { 258 | const results = [this.expr_1, this.expr_2, this.expr_3].map(d => d.build(args)); 259 | if ((results[0] >= results[1]) && (results[0] <= results[2])) { 260 | return true; 261 | } 262 | return false; 263 | } else if (operator === 'in') { 264 | const processExpr = (operand) => { 265 | this.expr_2 = Array.isArray(this.expr_2) ? this.expr_2 : [this.expr_2]; 266 | const tests = this.expr_2.map(d => d.build(args)); 267 | return tests.map(test => test(operand)).reduce((accu, next) => accu || next, false); 268 | }; 269 | return processExpr(this.expr_1.build(args)); 270 | } 271 | const results = [this.expr_1, this.expr_2].map(d => d.build(args)); 272 | operator = operator !== '=' ? operator : '=='; 273 | return fnGen(operator)(results[0])(results[1]); 274 | }; 275 | }; 276 | -------------------------------------------------------------------------------- /dist/feel-ast.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 ast = {}; 10 | 11 | /* Begin AST Node Constructors */ 12 | function ProgramNode(body, loc) { 13 | this.type = 'Program'; 14 | this.body = body; 15 | this.loc = loc; 16 | } 17 | 18 | function IntervalStartLiteralNode(intervalType, loc) { 19 | this.type = 'IntervalStartLiteral'; 20 | this.intervalType = intervalType; 21 | this.loc = loc; 22 | } 23 | 24 | function IntervalEndLiteralNode(intervalType, loc) { 25 | this.type = 'IntervalEndLiteral'; 26 | this.intervalType = intervalType; 27 | this.loc = loc; 28 | } 29 | 30 | function IntervalNode(intervalstart, startpoint, endpoint, intervalend, loc) { 31 | this.type = 'Interval'; 32 | this.intervalstart = intervalstart; 33 | this.startpoint = startpoint; 34 | this.endpoint = endpoint; 35 | this.intervalend = intervalend; 36 | this.loc = loc; 37 | } 38 | 39 | function SimplePositiveUnaryTestNode(operator, operand, loc) { 40 | this.type = 'SimplePositiveUnaryTest'; 41 | this.operator = operator; 42 | this.operand = operand; 43 | this.loc = loc; 44 | } 45 | 46 | function SimpleUnaryTestsNode(expr, not, loc) { 47 | this.type = 'SimpleUnaryTestsNode'; 48 | this.expr = expr; 49 | this.not = not; 50 | this.loc = loc; 51 | } 52 | 53 | function QualifiedNameNode(names, loc) { 54 | this.type = 'QualifiedName'; 55 | this.names = names; 56 | this.loc = loc; 57 | } 58 | 59 | function ArithmeticExpressionNode(operator, operand1, operand2, loc) { 60 | this.type = 'ArithmeticExpression'; 61 | this.operator = operator; 62 | this.operand_1 = operand1; 63 | this.operand_2 = operand2; 64 | this.loc = loc; 65 | } 66 | 67 | function SimpleExpressionsNode(simpleExpressions, loc) { 68 | this.type = 'SimpleExpressions'; 69 | this.simpleExpressions = simpleExpressions; 70 | this.loc = loc; 71 | } 72 | 73 | function NameNode(nameChars, loc) { 74 | this.type = 'Name'; 75 | this.nameChars = nameChars; 76 | this.loc = loc; 77 | } 78 | 79 | function LiteralNode(value, loc) { 80 | this.type = 'Literal'; 81 | this.value = value; 82 | this.loc = loc; 83 | } 84 | 85 | function DateTimeLiteralNode(symbol, params, loc) { 86 | this.type = 'DateTimeLiteral'; 87 | this.symbol = symbol; 88 | this.params = params; 89 | this.loc = loc; 90 | } 91 | 92 | function DecimalNumberNode(integer, decimal, loc) { 93 | this.type = 'DecimalNumberNode'; 94 | this.integer = integer; 95 | this.decimal = decimal; 96 | this.loc = loc; 97 | } 98 | 99 | function FunctionInvocationNode(fnName, params, loc) { 100 | this.type = 'FunctionInvocation'; 101 | this.fnName = fnName; 102 | this.params = params; 103 | this.loc = loc; 104 | } 105 | 106 | function PositionalParametersNode(params, loc) { 107 | this.type = 'PositionalParameters'; 108 | this.params = params; 109 | this.loc = loc; 110 | } 111 | 112 | function ComparisonExpressionNode(operator, expr1, expr2, expr3, loc) { 113 | this.type = 'ComparisonExpression'; 114 | this.operator = operator; 115 | this.expr_1 = expr1; 116 | this.expr_2 = expr2; 117 | this.expr_3 = expr3; 118 | this.loc = loc; 119 | } 120 | 121 | /* End AST Node Constructors */ 122 | 123 | /* Expose the AST Node Constructors */ 124 | ast.ProgramNode = ProgramNode; 125 | ast.IntervalStartLiteralNode = IntervalStartLiteralNode; 126 | ast.IntervalEndLiteralNode = IntervalEndLiteralNode; 127 | ast.IntervalNode = IntervalNode; 128 | ast.SimplePositiveUnaryTestNode = SimplePositiveUnaryTestNode; 129 | ast.SimpleUnaryTestsNode = SimpleUnaryTestsNode; 130 | ast.QualifiedNameNode = QualifiedNameNode; 131 | ast.ArithmeticExpressionNode = ArithmeticExpressionNode; 132 | ast.SimpleExpressionsNode = SimpleExpressionsNode; 133 | ast.NameNode = NameNode; 134 | ast.LiteralNode = LiteralNode; 135 | ast.DateTimeLiteralNode = DateTimeLiteralNode; 136 | ast.DecimalNumberNode = DecimalNumberNode; 137 | ast.FunctionInvocationNode = FunctionInvocationNode; 138 | ast.PositionalParametersNode = PositionalParametersNode; 139 | ast.ComparisonExpressionNode = ComparisonExpressionNode; 140 | 141 | module.exports = ast; 142 | -------------------------------------------------------------------------------- /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 , ")" ; -------------------------------------------------------------------------------- /dmn-input-variables.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HBTGmbH/dmn-eval-js/bb2780c11f322188f9397387deab9ce9e24cfef7/dmn-input-variables.png -------------------------------------------------------------------------------- /dmn-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/HBTGmbH/dmn-eval-js/bb2780c11f322188f9397387deab9ce9e24cfef7/dmn-output.png -------------------------------------------------------------------------------- /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 | 12 | // adding build methods to prototype of each constructor 13 | require('./feel-ast-parser')(ast); 14 | 15 | function extractOptional(optional, index) { 16 | return optional ? optional[index] : null; 17 | } 18 | 19 | function flatten(list) { 20 | return list.filter( d => d && d.length).reduce((recur, next) => { 21 | if(next && Array.isArray(next)) { 22 | return [].concat.call(recur, flatten(next)); 23 | } 24 | return [].concat.call(recur, next); 25 | }, []); 26 | } 27 | 28 | function extractList(list, index) { 29 | return list.map(element => element[index]); 30 | } 31 | 32 | function buildList(head, tail, index) { 33 | return [head].concat(extractList(tail, index)); 34 | } 35 | 36 | function buildName(head, tail, index) { 37 | return tail && tail.length ? [...head, ...flatten(tail)].join("") : head.join(""); 38 | } 39 | 40 | 41 | function buildComparisonExpression(head, tail, loc) { 42 | return tail.reduce((result, element) => { 43 | const operator = Array.isArray(element[1]) ? element[1][0] : element[1]; 44 | return new ast.ComparisonExpressionNode(operator, result, element[3], null, loc); 45 | }, head); 46 | } 47 | -------------------------------------------------------------------------------- /grammar/feel.pegjs: -------------------------------------------------------------------------------- 1 | Start 2 | = __ program:(ExpressionOrTests __)? 3 | { 4 | return new ast.ProgramNode(extractOptional(program,0),location()); 5 | } 6 | 7 | ExpressionOrTests 8 | = SimpleExpression 9 | / SimpleUnaryTests 10 | 11 | 12 | // 1. 13 | Expression 14 | = SimpleExpression 15 | 16 | // 4. 17 | ArithmeticExpression 18 | = Addition 19 | / Subtraction 20 | / Multiplication 21 | / Division 22 | / Exponentiation 23 | / ArithmeticNegation 24 | / BrackenedArithmeticExpression 25 | 26 | BrackenedArithmeticExpression 27 | = "(" __ expr:ArithmeticExpression __ ")" 28 | { 29 | return expr; 30 | } 31 | 32 | // 5. 33 | SimpleExpression 34 | = ArithmeticExpression 35 | / Comparison 36 | / SimpleValue 37 | 38 | // 6. 39 | SimpleExpressions 40 | = head:SimpleExpression tail:(__ "," __ SimpleExpression)* 41 | { 42 | return new ast.SimpleExpressionsNode(buildList(head,tail,3), location()); 43 | } 44 | 45 | // 7. 46 | SimplePositiveUnaryTest 47 | = head:(UnaryOperator __)? tail:Endpoint 48 | { 49 | return new ast.SimplePositiveUnaryTestNode(extractOptional(head,0),tail,location()); 50 | } 51 | / Interval 52 | 53 | UnaryOperator 54 | = "<=" 55 | / ">=" 56 | / "<" 57 | / ">" 58 | 59 | // 8. 60 | Interval 61 | = start:IntervalStart !(IntervalStart / IntervalEnd) __ first:Endpoint __ ".." __ second:Endpoint __ end:IntervalEnd 62 | { 63 | return new ast.IntervalNode(start,first,second,end,location()); 64 | } 65 | 66 | IntervalStart 67 | = OpenIntervalStart 68 | { 69 | return new ast.IntervalStartLiteralNode("<",location()); 70 | } 71 | / ClosedIntervalStart 72 | { 73 | return new ast.IntervalStartLiteralNode("<=",location()); 74 | } 75 | 76 | IntervalEnd 77 | = OpenIntervalEnd 78 | { 79 | return new ast.IntervalEndLiteralNode(">",location()); 80 | } 81 | / ClosedIntervalEnd 82 | { 83 | return new ast.IntervalEndLiteralNode(">=",location()); 84 | } 85 | 86 | // 9. 87 | OpenIntervalStart 88 | = "(" 89 | / "]" 90 | 91 | // 10. 92 | ClosedIntervalStart 93 | = "[" 94 | 95 | // 11. 96 | OpenIntervalEnd 97 | = ")" 98 | / "[" 99 | 100 | // 12. 101 | ClosedIntervalEnd 102 | = "]" 103 | 104 | // 13. 105 | SimplePositiveUnaryTests 106 | = head: SimplePositiveUnaryTest 107 | tail: (__ "," __ SimplePositiveUnaryTest)* 108 | { 109 | return buildList(head,tail,3); 110 | } 111 | 112 | // 14. 113 | SimpleUnaryTests 114 | = expr:SimplePositiveUnaryTests 115 | { 116 | return new ast.SimpleUnaryTestsNode(expr,null,location()); 117 | } 118 | / not:$NotToken __ "(" __ expr:SimplePositiveUnaryTests __ ")" 119 | { 120 | return new ast.SimpleUnaryTestsNode(expr,not,location()); 121 | } 122 | / "-" 123 | { 124 | return new ast.SimpleUnaryTestsNode(null,null,location()); 125 | } 126 | 127 | NotToken = "not" !NamePartChar 128 | 129 | // 18. 130 | Endpoint 131 | = ArithmeticExpression 132 | / SimpleValue 133 | 134 | // 19. 135 | SimpleValue 136 | = SimpleLiteral 137 | / FunctionInvocation 138 | / QualifiedName 139 | 140 | // 20. 141 | QualifiedName 142 | = head:Name tail: (__ "." __ Name)* 143 | { 144 | return new ast.QualifiedNameNode(buildList(head,tail,3),location()); 145 | } 146 | 147 | // 21. 148 | Addition 149 | = head:NonRecursiveSimpleExpressionForArithmeticExpression 150 | tail:(__ $("+") __ Expression) 151 | { return new ast.ArithmeticExpressionNode('+', head, tail[3], location()); } 152 | 153 | NonRecursiveSimpleExpressionForArithmeticExpression 154 | = BrackenedArithmeticExpression 155 | / SimpleValue 156 | 157 | // 22. 158 | Subtraction 159 | = head:NonRecursiveSimpleExpressionForArithmeticExpression 160 | tail:(__ $("-") __ Expression) 161 | { return new ast.ArithmeticExpressionNode('-', head, tail[3], location()); } 162 | 163 | // 23. 164 | Multiplication 165 | = head:NonRecursiveSimpleExpressionForArithmeticExpression 166 | tail:(__ $("*" !"*") __ Expression) 167 | { return new ast.ArithmeticExpressionNode('*', head, tail[3], location()); } 168 | 169 | // 24. 170 | Division 171 | = head:NonRecursiveSimpleExpressionForArithmeticExpression 172 | tail:(__ $("/") __ Expression) 173 | { return new ast.ArithmeticExpressionNode('/', head, tail[3], location()); } 174 | 175 | // 25. 176 | Exponentiation 177 | = head:NonRecursiveSimpleExpressionForArithmeticExpression 178 | tail:(__ $("**") __ Expression) 179 | { return new ast.ArithmeticExpressionNode('**', head, tail[3], location()); } 180 | 181 | // 26. 182 | ArithmeticNegation 183 | = $("-") expr: (__ expr:Expression) 184 | { 185 | return new ast.ArithmeticExpressionNode('-', null, expr[1], location()); 186 | } 187 | 188 | // 27. 189 | Name 190 | = !ReservedWord head:NameStart tail:(__ (!ReservedWord) __ NamePart)* 191 | { 192 | return new ast.NameNode(buildName(head,tail,0),location()); 193 | } 194 | 195 | ReservedWord 196 | = Keyword 197 | / DateTimeKeyword 198 | / NullLiteral 199 | / BooleanLiteral 200 | 201 | // 28. 202 | NameStart 203 | = head:NameStartChar tail:(NamePartChar)* 204 | { 205 | return buildList(head,tail,0); 206 | } 207 | 208 | // 29. 209 | NamePart 210 | = head:NamePartChar tail:(NamePartChar)* 211 | { 212 | return buildList(head,tail,0); 213 | } 214 | 215 | // 30. 216 | NameStartChar 217 | = [?] 218 | / [A-Z] 219 | / [_] 220 | / [a-z] 221 | / NameStartUnicodeChar 222 | 223 | NameStartUnicodeChar = [\u0300-\u036F\u0483-\u0487\u0591-\u05BD\u05BF\u05C1-\u05C2\u05C4-\u05C5\u05C7\u0610-\u061A\u064B-\u065F\u0670\u06D6-\u06DC\u06DF-\u06E4\u06E7-\u06E8\u06EA-\u06ED\u0711\u0730-\u074A\u07A6-\u07B0\u07EB-\u07F3\u0816-\u0819\u081B-\u0823\u0825-\u0827\u0829-\u082D\u0859-\u085B\u08E3-\u0902\u093A\u093C\u0941-\u0948\u094D\u0951-\u0957\u0962-\u0963\u0981\u09BC\u09C1-\u09C4\u09CD\u09E2-\u09E3\u0A01-\u0A02\u0A3C\u0A41-\u0A42\u0A47-\u0A48\u0A4B-\u0A4D\u0A51\u0A70-\u0A71\u0A75\u0A81-\u0A82\u0ABC\u0AC1-\u0AC5\u0AC7-\u0AC8\u0ACD\u0AE2-\u0AE3\u0B01\u0B3C\u0B3F\u0B41-\u0B44\u0B4D\u0B56\u0B62-\u0B63\u0B82\u0BC0\u0BCD\u0C00\u0C3E-\u0C40\u0C46-\u0C48\u0C4A-\u0C4D\u0C55-\u0C56\u0C62-\u0C63\u0C81\u0CBC\u0CBF\u0CC6\u0CCC-\u0CCD\u0CE2-\u0CE3\u0D01\u0D41-\u0D44\u0D4D\u0D62-\u0D63\u0DCA\u0DD2-\u0DD4\u0DD6\u0E31\u0E34-\u0E3A\u0E47-\u0E4E\u0EB1\u0EB4-\u0EB9\u0EBB-\u0EBC\u0EC8-\u0ECD\u0F18-\u0F19\u0F35\u0F37\u0F39\u0F71-\u0F7E\u0F80-\u0F84\u0F86-\u0F87\u0F8D-\u0F97\u0F99-\u0FBC\u0FC6\u102D-\u1030\u1032-\u1037\u1039-\u103A\u103D-\u103E\u1058-\u1059\u105E-\u1060\u1071-\u1074\u1082\u1085-\u1086\u108D\u109D\u135D-\u135F\u1712-\u1714\u1732-\u1734\u1752-\u1753\u1772-\u1773\u17B4-\u17B5\u17B7-\u17BD\u17C6\u17C9-\u17D3\u17DD\u180B-\u180D\u18A9\u1920-\u1922\u1927-\u1928\u1932\u1939-\u193B\u1A17-\u1A18\u1A1B\u1A56\u1A58-\u1A5E\u1A60\u1A62\u1A65-\u1A6C\u1A73-\u1A7C\u1A7F\u1AB0-\u1ABD\u1B00-\u1B03\u1B34\u1B36-\u1B3A\u1B3C\u1B42\u1B6B-\u1B73\u1B80-\u1B81\u1BA2-\u1BA5\u1BA8-\u1BA9\u1BAB-\u1BAD\u1BE6\u1BE8-\u1BE9\u1BED\u1BEF-\u1BF1\u1C2C-\u1C33\u1C36-\u1C37\u1CD0-\u1CD2\u1CD4-\u1CE0\u1CE2-\u1CE8\u1CED\u1CF4\u1CF8-\u1CF9\u1DC0-\u1DF5\u1DFC-\u1DFF\u20D0-\u20DC\u20E1\u20E5-\u20F0\u2CEF-\u2CF1\u2D7F\u2DE0-\u2DFF\u302A-\u302D\u3099-\u309A\uA66F\uA674-\uA67D\uA69E-\uA69F\uA6F0-\uA6F1\uA802\uA806\uA80B\uA825-\uA826\uA8C4\uA8E0-\uA8F1\uA926-\uA92D\uA947-\uA951\uA980-\uA982\uA9B3\uA9B6-\uA9B9\uA9BC\uA9E5\uAA29-\uAA2E\uAA31-\uAA32\uAA35-\uAA36\uAA43\uAA4C\uAA7C\uAAB0\uAAB2-\uAAB4\uAAB7-\uAAB8\uAABE-\uAABF\uAAC1\uAAEC-\uAAED\uAAF6\uABE5\uABE8\uABED\uFB1E\uFE00-\uFE0F\uFE20-\uFE2F] 224 | 225 | // 31. 226 | NamePartChar 227 | = NameStartChar 228 | / Digit 229 | / NamePartUnicodeChar 230 | / ['] 231 | 232 | NamePartUnicodeChar = [\u0B70\u0300-\u036F\u203F-\u2040] 233 | 234 | // 33. 235 | SimpleLiteral 236 | = NumericLiteral 237 | / StringLiteral 238 | / BooleanLiteral 239 | / DateTimeLiteral 240 | / NullLiteral 241 | 242 | // 34. 243 | StringLiteral "string" 244 | = '"' chars:DoubleStringCharacter* '"' { 245 | return new ast.LiteralNode(chars.join(""),location()); 246 | } 247 | / "'" chars:SingleStringCharacter* "'" { 248 | return new ast.LiteralNode(chars.join(""),location()); 249 | } 250 | 251 | DoubleStringCharacter 252 | = !('"' / "\\" / LineTerminator) SourceCharacter { return text(); } 253 | / "\\" sequence:EscapeSequence { return sequence; } 254 | / LineContinuation 255 | 256 | SingleStringCharacter 257 | = !("'" / "\\" / LineTerminator) SourceCharacter { return text(); } 258 | / "\\" sequence:EscapeSequence { return sequence; } 259 | / LineContinuation 260 | 261 | SourceCharacter 262 | = . 263 | 264 | LineContinuation 265 | = "\\" LineTerminatorSequence { return ""; } 266 | 267 | EscapeSequence 268 | = CharacterEscapeSequence 269 | 270 | CharacterEscapeSequence 271 | = SingleEscapeCharacter 272 | 273 | SingleEscapeCharacter 274 | = "'" 275 | / '"' 276 | / "\\" 277 | / "b" { return "\b"; } 278 | / "f" { return "\f"; } 279 | / "n" { return "\n"; } 280 | / "r" { return "\r"; } 281 | / "t" { return "\t"; } 282 | / "v" { return "\v"; } 283 | 284 | LineTerminator 285 | = [\n\r\u2028\u2029] 286 | 287 | LineTerminatorSequence "end of line" 288 | = "\n" 289 | / "\r\n" 290 | / "\r" 291 | / "\u2028" 292 | / "\u2029" 293 | 294 | // 35. 295 | BooleanLiteral 296 | = $TrueToken 297 | { 298 | return new ast.LiteralNode(true, location()); 299 | } 300 | / $FalseToken 301 | { 302 | return new ast.LiteralNode(false, location()); 303 | } 304 | 305 | TrueToken = $("true"/"TRUE"/"True") !NamePartChar 306 | FalseToken = $("false"/"FALSE"/"False") !NamePartChar 307 | 308 | // 36. 309 | NumericLiteral 310 | = negative:("-")? __ number:DecimalNumber 311 | { 312 | return new ast.LiteralNode(Number((negative || "") + number),location()); 313 | } 314 | 315 | DecimalNumber 316 | = integer:Digits "." decimal:Digits 317 | { 318 | return integer.join("") + "." + decimal.join(""); 319 | } 320 | / "." decimal:Digits 321 | { 322 | return "." + decimal.join(""); 323 | } 324 | / integer:Digits 325 | { 326 | return integer.join(""); 327 | } 328 | 329 | // 37. 330 | Digit 331 | = [0-9] 332 | 333 | // 38. 334 | Digits 335 | = [0-9]+ 336 | 337 | // 39. 338 | DateTimeLiteral 339 | = symbol: DateTimeKeyword "(" __ head:Expression tail:(__ "," __ Expression)* __ ")" 340 | { 341 | return new ast.DateTimeLiteralNode(symbol[0], buildList(head, tail, 3), location()); 342 | } 343 | 344 | Keyword 345 | = TrueToken 346 | / FalseToken 347 | / NullToken 348 | / NotToken 349 | 350 | DateTimeKeyword 351 | = "date and time" !NamePartChar 352 | / "time" !NamePartChar 353 | / "date" !NamePartChar 354 | / "duration" !NamePartChar 355 | 356 | // 51. 357 | Comparison 358 | = head:NonRecursiveSimpleExpressionForComparison tail:(__ ComparisonOperator __ Expression)+ 359 | { return buildComparisonExpression(head,tail,location()); } 360 | 361 | NonRecursiveSimpleExpressionForComparison 362 | = ArithmeticExpression 363 | / SimpleValue 364 | 365 | ComparisonOperator 366 | = "=" 367 | / "!=" 368 | / $"<" !"=" 369 | / "<=" 370 | / $">" !"=" 371 | / ">=" 372 | 373 | FunctionInvocation 374 | = fnName:QualifiedName __ "(" params:(__ (PositionalParameters))? __ ")" 375 | { 376 | return new ast.FunctionInvocationNode(fnName,extractOptional(params,1),location()); 377 | } 378 | 379 | PositionalParameters 380 | = head:Expression tail:(__ "," __ Expression)* 381 | { 382 | return new ast.PositionalParametersNode(buildList(head,tail,3),location()); 383 | } 384 | 385 | NullLiteral 386 | = $NullToken 387 | { 388 | return new ast.LiteralNode(null, location()); 389 | } 390 | 391 | NullToken = "null" !NamePartChar 392 | 393 | __ 394 | = (WhiteSpace)* 395 | 396 | WhiteSpace "whitespace" 397 | = "\t" 398 | / "\v" 399 | / "\f" 400 | / " " 401 | / "\u00A0" 402 | / "\uFEFF" 403 | / Zs 404 | 405 | Zs = [\u0020\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000] 406 | -------------------------------------------------------------------------------- /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'], () => gulp.src(['./temp/feel-initializer.js', './grammar/feel.pegjs']) 45 | .pipe(concat('feel.pegjs')) 46 | .pipe(gulp.dest('./src/'))); 47 | 48 | 49 | gulp.task('clean:temp', ['initialize:feel', 'concat:feel'], () => gulp.src('./temp', { 50 | read: false, 51 | }) 52 | .pipe(clean())); 53 | 54 | gulp.task('clean:dist:feel', ['src:lint'], () => gulp.src('./dist/feel.js', { 55 | read: false, 56 | }) 57 | .pipe(clean())); 58 | 59 | gulp.task('clean:dist:feel:ast', ['src:lint'], () => gulp.src('./dist/feel-ast*.js', { 60 | read: false, 61 | }) 62 | .pipe(clean())); 63 | 64 | gulp.task('clean:src:feel', () => gulp.src('./src/feel.pegjs', { 65 | read: false, 66 | }) 67 | .pipe(clean())); 68 | 69 | gulp.task('generate:parser',['clean:dist:feel'], () => gulp.src('src/feel.pegjs') 70 | .pipe(peg({ 71 | format: 'commonjs', 72 | cache: true, 73 | allowedStartRules: ["Start", "SimpleExpressions", "SimpleUnaryTests"] 74 | })) 75 | .pipe(gulp.dest('./dist'))); 76 | 77 | gulp.task('dist:feel:ast', ['clean:dist:feel:ast'], () => gulp.src('src/feel-ast.js') 78 | .pipe(gulp.dest('./dist'))); 79 | 80 | gulp.task('dist:feel:ast:parser', ['clean:dist:feel:ast'], () => gulp.src('src/feel-ast-parser.js') 81 | .pipe(gulp.dest('./dist'))); 82 | 83 | 84 | gulp.task('mocha', () => gulp.src(['test/*.js'], { 85 | read: false, 86 | }) 87 | .pipe(mocha({ 88 | reporter: 'list', 89 | })) 90 | .on('error', gutil.log)); 91 | 92 | 93 | gulp.task('lint', () => { 94 | return gulp.src(['**/*.js','!node_modules/**']) 95 | .pipe(log('linting')) 96 | .pipe(eslint()) 97 | .pipe(eslint.format()) 98 | .pipe(eslint.failAfterError()); 99 | }); 100 | 101 | gulp.task('src:lint', ()=>{ 102 | return gulp.src(['src/*.js']) 103 | .pipe(eslint()) 104 | .pipe(eslint.format()) 105 | .pipe(eslint.failAfterError()); 106 | }); 107 | 108 | gulp.task('utils:lint', ()=>{ 109 | return gulp.src(['utils/*.js']) 110 | .pipe(eslint()) 111 | .pipe(eslint.format()) 112 | .pipe(eslint.failAfterError()); 113 | }); 114 | 115 | gulp.task('pre-test-ci', function () { 116 | return gulp.src(['./dist/**/*.js','./utils/**/*.js','!./dist/**/feel.js','!./utils/**/index.js']) 117 | .pipe(istanbul()) 118 | .pipe(istanbul.hookRequire()); 119 | }); 120 | 121 | gulp.task('test-ci', ['pre-test-ci'], function () { 122 | return gulp.src(['test/**/*.spec.js']) 123 | .pipe(mocha()) 124 | .pipe(istanbul.writeReports({ 125 | dir: './coverage', 126 | reporters: [ 'lcovonly'], 127 | reportOpts: { dir: './coverage' } 128 | })) 129 | .pipe(istanbul.enforceThresholds({ thresholds:{ global: {statements: 85, branches: 70, lines: 85, functions: 90 }} })); 130 | }); 131 | 132 | gulp.task('test-ci-html', ['pre-test-ci'], function () { 133 | return gulp.src(['test/**/*.spec.js']) 134 | .pipe(mocha()) 135 | .pipe(istanbul.writeReports({ 136 | dir: './coverage', 137 | reporters: [ 'lcov'], 138 | reportOpts: { dir: './coverage' } 139 | })) 140 | .pipe(istanbul.enforceThresholds({ thresholds:{ global: {statements: 85, branches: 70, lines: 85, functions: 90 }} })); 141 | }); 142 | 143 | gulp.task('build', ['initialize:feel', 'clean:src:feel', 'concat:feel', 'clean:temp']); 144 | 145 | gulp.task('generate', ['generate:parser']); 146 | 147 | gulp.task('default', ['build', 'generate', 'mocha']); 148 | 149 | gulp.task('watch', () => { 150 | gulp.watch('./grammar/*', ['build']); 151 | gulp.watch('./src/*.pegjs',['generate:parser']); 152 | gulp.watch('./src/*.js', ['dist:feel:ast', 'dist:feel:ast:parser']); 153 | gulp.watch('./utils/*.js', ['utils:lint']); 154 | }); 155 | -------------------------------------------------------------------------------- /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 | // require('nashorn-polyfill'); 9 | // require('core-js/modules/es6.object.assign'); 10 | 11 | const decisionTable = require('./utils/helper/decision-table-xml.js'); 12 | const dateTime = require('./utils/built-in-functions/date-time-functions'); 13 | 14 | const dmnEvalJs = { 15 | decisionTable, 16 | dateTime, 17 | }; 18 | 19 | dmnEvalJs.use = function (plugin) { 20 | plugin.call(this); 21 | }; 22 | 23 | module.exports = dmnEvalJs; 24 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hbtgmbh/dmn-eval-js", 3 | "version": "1.5.0", 4 | "description": "Evaluation of DMN 1.1 decision tables, limited to S-FEEL (Simple Friendly Enough Expression Language)", 5 | "license": "MIT", 6 | "main": "index.js", 7 | "scripts": { 8 | "lint": "eslint .", 9 | "lintfix": "eslint . --fix", 10 | "test": "./node_modules/.bin/mocha ./test/**/*.spec.js", 11 | "build": "./node_modules/.bin/gulp", 12 | "precommit-msg": "echo 'Pre-commit checks...' && exit 0" 13 | }, 14 | "pre-commit": [ 15 | "precommit-msg", 16 | "lint" 17 | ], 18 | "engines": { 19 | "node": ">=6.9.2" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git@github.com:HBTGmbH/dmn-eval-js.git" 24 | }, 25 | "keywords": [ 26 | "DMN 1.1", 27 | "FEEL", 28 | "S-FEEL", 29 | "Rule", 30 | "Rule Engine", 31 | "Decision", 32 | "Decision Table" 33 | ], 34 | "author": "Andre Hegerath ", 35 | "dependencies": { 36 | "big.js": "^3.1.3", 37 | "dmn-moddle": "^4.0.0", 38 | "lodash": "^4.17.4", 39 | "loglevel": "^1.6.1", 40 | "moment": "^2.21.0", 41 | "moment-timezone": "^0.5.13" 42 | }, 43 | "devDependencies": { 44 | "chai": "^4.1.2", 45 | "chai-arrays": "^2.0.0", 46 | "chalk": "^1.1.3", 47 | "eslint": "^3.19.0", 48 | "eslint-config-airbnb": "^14.1.0", 49 | "eslint-plugin-import": "^2.9.0", 50 | "eslint-plugin-jsx-a11y": "^4.0.0", 51 | "eslint-plugin-react": "^6.9.0", 52 | "gulp": "^3.9.1", 53 | "gulp-clean": "^0.3.2", 54 | "gulp-concat": "^2.6.1", 55 | "gulp-eslint": "^3.0.1", 56 | "gulp-if": "^2.0.2", 57 | "gulp-insert": "^0.5.0", 58 | "gulp-istanbul": "^1.1.1", 59 | "gulp-mocha": "^3.0.1", 60 | "gulp-util": "^3.0.8", 61 | "gulp-watch": "^4.3.11", 62 | "istanbul": "^0.4.5", 63 | "minimist": "^1.2.0", 64 | "mocha": "^3.2.0", 65 | "pegjs": "^0.10.0", 66 | "pegjs-backtrace": "^0.1.1", 67 | "pre-commit": "^1.2.2", 68 | "through2": "^2.0.3" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/feel-ast-parser.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 _ = require('lodash'); 10 | const fnGen = require('../utils/helper/fn-generator'); 11 | const addKwargs = require('../utils/helper/add-kwargs'); 12 | const builtInFns = require('../utils/built-in-functions'); 13 | const resolveName = require('../utils/helper/name-resolution.js'); 14 | const logger = require('loglevel').getLogger('dmn-eval-js'); 15 | 16 | module.exports = function (ast) { 17 | ast.ProgramNode.prototype.build = function (data = {}, env = {}, type = 'output') { 18 | let args = {}; 19 | if (!data.isContextBuilt) { 20 | const context = Object.assign({}, data, builtInFns); 21 | args = Object.assign({}, { context }, env); 22 | args.isContextBuilt = true; 23 | } else { 24 | args = data; 25 | } 26 | // bodybuilding starts here... 27 | // let's pump some code ;) 28 | const result = this.body.build(args); 29 | if (type === 'input') { 30 | if (typeof result === 'function') { 31 | return result; 32 | } 33 | const fnResult = function (x) { 34 | return x === result; 35 | }; 36 | return fnResult; 37 | } 38 | return result; 39 | }; 40 | 41 | ast.IntervalStartLiteralNode.prototype.build = function () { 42 | return fnGen(this.intervalType); 43 | }; 44 | 45 | ast.IntervalEndLiteralNode.prototype.build = function () { 46 | return fnGen(this.intervalType); 47 | }; 48 | 49 | ast.IntervalNode.prototype.build = function (args) { 50 | const startpoint = this.startpoint.build(args); 51 | const endpoint = this.endpoint.build(args); 52 | return (x) => { 53 | const startValue = this.intervalstart.build()(startpoint)(x); 54 | const endValue = this.intervalend.build()(endpoint)(x); 55 | return startValue === undefined || endValue === undefined ? undefined : startValue && endValue; 56 | }; 57 | }; 58 | 59 | ast.SimplePositiveUnaryTestNode.prototype.build = function (args) { 60 | const result = this.operand.build(args); 61 | 62 | // hack to treat input expressions as input variables and let functions in input entries reference them 63 | // for example: starts with(name, prefix) 64 | // where "name" is the input expression 65 | // for this to work, if the result of the function is true (like in the example above), that value cannot be 66 | // compared with the the evaluated input expression (which is the value of the input variable), so we must 67 | // patch the comparison here 68 | if (args.context._inputVariableName && this.operand.type === 'FunctionInvocation' && this.operand.params) { 69 | // patch only if there is an input variable and the simple positive unary test contains a function directly, 70 | // where the input variable in a parameter of that function 71 | const nodeIsQualifiedNameOfInputVariable = node => 72 | node.type === 'QualifiedName' && node.names.map(nameNode => nameNode.nameChars).join('.') === args.context._inputVariableName; 73 | const inputVariableParameter = (this.operand.params.params || []).find(node => nodeIsQualifiedNameOfInputVariable(node)); 74 | if (inputVariableParameter) { 75 | if (result === true) { 76 | // if the function evaluates to true, compare the evaluated input expression with the evaluated input variable, 77 | // not with the result of the function evaluation 78 | return fnGen(this.operator || '==')(_, inputVariableParameter.build(args)); 79 | } else if (result === false) { 80 | // if the function evaluates to false, the simple positive unary test should always evaluate to false 81 | return () => false; 82 | } 83 | } 84 | } 85 | 86 | return fnGen(this.operator || '==')(_, result); 87 | }; 88 | 89 | ast.SimpleUnaryTestsNode.prototype.build = function (data = {}) { 90 | const context = Object.assign({}, data, builtInFns); 91 | const args = { context }; 92 | if (this.expr) { 93 | const results = this.expr.map(d => d.build(args)); 94 | if (this.not) { 95 | const negResults = results.map(result => args.context.not(result)); 96 | return x => negResults.reduce((result, next) => { 97 | const nextValue = next(x); 98 | return (result === false || nextValue === false) ? false : ((result === undefined || nextValue === undefined) ? undefined : (result && nextValue)); 99 | }, true); 100 | } 101 | return x => results.reduce((result, next) => { 102 | const nextValue = next(x); 103 | return (result === true || nextValue === true) ? true : ((result === undefined || nextValue === undefined) ? undefined : (result || nextValue)); 104 | }, false); 105 | } 106 | return () => true; 107 | }; 108 | 109 | ast.QualifiedNameNode.prototype.build = function (args, doNotWarnIfUndefined = false) { 110 | const [first, ...remaining] = this.names; 111 | const buildNameNode = (name) => { 112 | const result = { nameNode: name, value: name.build(null, false) }; 113 | return result; 114 | }; 115 | const processRemaining = (firstResult, firstExpression) => remaining.map(buildNameNode) 116 | .reduce((prev, next) => { 117 | if (prev.value === undefined) { 118 | return prev; 119 | } 120 | return { value: prev.value[next.value], expression: `${prev.expression}.${next.nameNode.nameChars}` }; 121 | }, { value: firstResult, expression: firstExpression }); 122 | 123 | const firstResult = first.build(args); 124 | if (remaining.length) { 125 | const fullResult = processRemaining(firstResult, first.nameChars); 126 | if (fullResult.value === undefined) { 127 | if (!doNotWarnIfUndefined) { 128 | logger.info(`'${fullResult.expression}' resolved to undefined`); 129 | } 130 | } 131 | return fullResult.value; 132 | } 133 | if (firstResult === undefined) { 134 | if (!doNotWarnIfUndefined) { 135 | logger.info(`'${first.nameChars}' resolved to undefined`); 136 | } 137 | } 138 | return firstResult; 139 | }; 140 | 141 | ast.ArithmeticExpressionNode.prototype.build = function (args) { 142 | const operandsResult = [this.operand_1, this.operand_2].map((op) => { 143 | if (op === null) { 144 | return 0; 145 | } 146 | return op.build(args); 147 | }); 148 | return fnGen(this.operator)(operandsResult[0])(operandsResult[1]); 149 | }; 150 | 151 | ast.SimpleExpressionsNode.prototype.build = function (data = {}, env = {}) { 152 | let context = {}; 153 | if (!data.isBuiltInFn) { 154 | context = Object.assign({}, data, builtInFns, { isBuiltInFn: true }); 155 | } else { 156 | context = data; 157 | } 158 | const args = Object.assign({}, { context }, env); 159 | return this.simpleExpressions.map(d => d.build(args)); 160 | }; 161 | 162 | // _fetch is used to return the name string or 163 | // the value extracted from context or kwargs using the name string 164 | ast.NameNode.prototype.build = function (args, _fetch = true) { 165 | const name = this.nameChars; 166 | if (!_fetch) { 167 | return name; 168 | } 169 | 170 | return resolveName(name, args); 171 | }; 172 | 173 | ast.LiteralNode.prototype.build = function () { 174 | return this.value; 175 | }; 176 | 177 | ast.DateTimeLiteralNode.prototype.build = function (args) { 178 | const fn = args.context[this.symbol]; 179 | const paramsResult = this.params.map(d => d.build(args)); 180 | let result; 181 | if (!paramsResult.includes(undefined)) { 182 | result = fn(...paramsResult); 183 | } 184 | return result; 185 | }; 186 | 187 | // Invoking function defined as boxed expression in the context entry 188 | // See ast.FunctionDefinitionNode for details on declaring function 189 | // Function supports positional as well as named parameters 190 | ast.FunctionInvocationNode.prototype.build = function (args) { 191 | const processFormalParameters = (formalParams) => { 192 | const values = this.params.build(args); 193 | if (formalParams && values && Array.isArray(values)) { 194 | const kwParams = values.reduce((recur, next, i) => { 195 | const obj = {}; 196 | obj[formalParams[i]] = next; 197 | return Object.assign({}, recur, obj); 198 | }, {}); 199 | return addKwargs(args, kwParams); 200 | } 201 | return addKwargs(args, values); 202 | }; 203 | 204 | const processUserDefinedFunction = (fnMeta) => { 205 | const fn = fnMeta.fn; 206 | const formalParams = fnMeta.params; 207 | 208 | if (formalParams) { 209 | return fn.build(processFormalParameters(formalParams)); 210 | } 211 | return fn.build(args); 212 | }; 213 | 214 | const processInBuiltFunction = (fnMeta) => { 215 | const doNotWarnIfUndefined = fnMeta.name === 'defined'; 216 | const values = this.params.build(args, doNotWarnIfUndefined); 217 | if (Array.isArray(values)) { 218 | return fnMeta(...[...values, args.context]); 219 | } 220 | return fnMeta(Object.assign({}, args.context, args.kwargs), values); 221 | }; 222 | 223 | const processDecision = (fnMeta) => { 224 | const expr = fnMeta.expr; 225 | if (expr.body instanceof ast.FunctionDefinitionNode) { 226 | const exprResult = expr.body.build(args); 227 | return processUserDefinedFunction(exprResult); 228 | } 229 | const formalParametersResult = processFormalParameters(); 230 | return expr.build(formalParametersResult); 231 | }; 232 | 233 | const processFnMeta = (fnMeta) => { 234 | if (typeof fnMeta === 'function') { 235 | return processInBuiltFunction(fnMeta); 236 | } else if (typeof fnMeta === 'object' && fnMeta.isDecision) { 237 | return processDecision(fnMeta); 238 | } 239 | return processUserDefinedFunction(fnMeta); 240 | }; 241 | 242 | const fnNameResult = this.fnName.build(args); 243 | let result; 244 | if (fnNameResult !== undefined) { 245 | result = processFnMeta(fnNameResult); 246 | } 247 | return result; 248 | }; 249 | 250 | ast.PositionalParametersNode.prototype.build = function (args, doNotWarnIfUndefined = false) { 251 | const results = this.params.map(d => d.build(args, doNotWarnIfUndefined)); 252 | return results; 253 | }; 254 | 255 | ast.ComparisonExpressionNode.prototype.build = function (args) { 256 | let operator = this.operator; 257 | if (operator === 'between') { 258 | const results = [this.expr_1, this.expr_2, this.expr_3].map(d => d.build(args)); 259 | if ((results[0] >= results[1]) && (results[0] <= results[2])) { 260 | return true; 261 | } 262 | return false; 263 | } else if (operator === 'in') { 264 | const processExpr = (operand) => { 265 | this.expr_2 = Array.isArray(this.expr_2) ? this.expr_2 : [this.expr_2]; 266 | const tests = this.expr_2.map(d => d.build(args)); 267 | return tests.map(test => test(operand)).reduce((accu, next) => accu || next, false); 268 | }; 269 | return processExpr(this.expr_1.build(args)); 270 | } 271 | const results = [this.expr_1, this.expr_2].map(d => d.build(args)); 272 | operator = operator !== '=' ? operator : '=='; 273 | return fnGen(operator)(results[0])(results[1]); 274 | }; 275 | }; 276 | -------------------------------------------------------------------------------- /src/feel-ast.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 ast = {}; 10 | 11 | /* Begin AST Node Constructors */ 12 | function ProgramNode(body, loc) { 13 | this.type = 'Program'; 14 | this.body = body; 15 | this.loc = loc; 16 | } 17 | 18 | function IntervalStartLiteralNode(intervalType, loc) { 19 | this.type = 'IntervalStartLiteral'; 20 | this.intervalType = intervalType; 21 | this.loc = loc; 22 | } 23 | 24 | function IntervalEndLiteralNode(intervalType, loc) { 25 | this.type = 'IntervalEndLiteral'; 26 | this.intervalType = intervalType; 27 | this.loc = loc; 28 | } 29 | 30 | function IntervalNode(intervalstart, startpoint, endpoint, intervalend, loc) { 31 | this.type = 'Interval'; 32 | this.intervalstart = intervalstart; 33 | this.startpoint = startpoint; 34 | this.endpoint = endpoint; 35 | this.intervalend = intervalend; 36 | this.loc = loc; 37 | } 38 | 39 | function SimplePositiveUnaryTestNode(operator, operand, loc) { 40 | this.type = 'SimplePositiveUnaryTest'; 41 | this.operator = operator; 42 | this.operand = operand; 43 | this.loc = loc; 44 | } 45 | 46 | function SimpleUnaryTestsNode(expr, not, loc) { 47 | this.type = 'SimpleUnaryTestsNode'; 48 | this.expr = expr; 49 | this.not = not; 50 | this.loc = loc; 51 | } 52 | 53 | function QualifiedNameNode(names, loc) { 54 | this.type = 'QualifiedName'; 55 | this.names = names; 56 | this.loc = loc; 57 | } 58 | 59 | function ArithmeticExpressionNode(operator, operand1, operand2, loc) { 60 | this.type = 'ArithmeticExpression'; 61 | this.operator = operator; 62 | this.operand_1 = operand1; 63 | this.operand_2 = operand2; 64 | this.loc = loc; 65 | } 66 | 67 | function SimpleExpressionsNode(simpleExpressions, loc) { 68 | this.type = 'SimpleExpressions'; 69 | this.simpleExpressions = simpleExpressions; 70 | this.loc = loc; 71 | } 72 | 73 | function NameNode(nameChars, loc) { 74 | this.type = 'Name'; 75 | this.nameChars = nameChars; 76 | this.loc = loc; 77 | } 78 | 79 | function LiteralNode(value, loc) { 80 | this.type = 'Literal'; 81 | this.value = value; 82 | this.loc = loc; 83 | } 84 | 85 | function DateTimeLiteralNode(symbol, params, loc) { 86 | this.type = 'DateTimeLiteral'; 87 | this.symbol = symbol; 88 | this.params = params; 89 | this.loc = loc; 90 | } 91 | 92 | function DecimalNumberNode(integer, decimal, loc) { 93 | this.type = 'DecimalNumberNode'; 94 | this.integer = integer; 95 | this.decimal = decimal; 96 | this.loc = loc; 97 | } 98 | 99 | function FunctionInvocationNode(fnName, params, loc) { 100 | this.type = 'FunctionInvocation'; 101 | this.fnName = fnName; 102 | this.params = params; 103 | this.loc = loc; 104 | } 105 | 106 | function PositionalParametersNode(params, loc) { 107 | this.type = 'PositionalParameters'; 108 | this.params = params; 109 | this.loc = loc; 110 | } 111 | 112 | function ComparisonExpressionNode(operator, expr1, expr2, expr3, loc) { 113 | this.type = 'ComparisonExpression'; 114 | this.operator = operator; 115 | this.expr_1 = expr1; 116 | this.expr_2 = expr2; 117 | this.expr_3 = expr3; 118 | this.loc = loc; 119 | } 120 | 121 | /* End AST Node Constructors */ 122 | 123 | /* Expose the AST Node Constructors */ 124 | ast.ProgramNode = ProgramNode; 125 | ast.IntervalStartLiteralNode = IntervalStartLiteralNode; 126 | ast.IntervalEndLiteralNode = IntervalEndLiteralNode; 127 | ast.IntervalNode = IntervalNode; 128 | ast.SimplePositiveUnaryTestNode = SimplePositiveUnaryTestNode; 129 | ast.SimpleUnaryTestsNode = SimpleUnaryTestsNode; 130 | ast.QualifiedNameNode = QualifiedNameNode; 131 | ast.ArithmeticExpressionNode = ArithmeticExpressionNode; 132 | ast.SimpleExpressionsNode = SimpleExpressionsNode; 133 | ast.NameNode = NameNode; 134 | ast.LiteralNode = LiteralNode; 135 | ast.DateTimeLiteralNode = DateTimeLiteralNode; 136 | ast.DecimalNumberNode = DecimalNumberNode; 137 | ast.FunctionInvocationNode = FunctionInvocationNode; 138 | ast.PositionalParametersNode = PositionalParametersNode; 139 | ast.ComparisonExpressionNode = ComparisonExpressionNode; 140 | 141 | module.exports = ast; 142 | -------------------------------------------------------------------------------- /test/arithmetic-expression/date-time-duration-arithmetic-expression.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | const chalk = require('chalk'); 6 | const chai = require('chai'); 7 | const expect = chai.expect; 8 | const FEEL = require('../../dist/feel'); 9 | 10 | describe(chalk.blue('Date/time/duration expression test'), function() { 11 | 12 | it('should do date subtraction and return a duration', function() { 13 | const text = 'date("2012-12-25") - date("2012-12-24")'; 14 | const parsedGrammar = FEEL.parse(text); 15 | const result = parsedGrammar.build(); 16 | expect(result.isDuration).to.be.true; 17 | 18 | expect(result.isDtd).to.be.true; 19 | expect(result.days).to.equal(1); 20 | }); 21 | 22 | it('should do throw error for date addition', function() { 23 | const text = 'date("2012-12-25") + date("2012-12-24")'; 24 | const parsedGrammar = FEEL.parse(text); 25 | try { 26 | parsedGrammar.build(); 27 | fail('Expected error to be thrown'); 28 | } catch (err) { 29 | expect(err.message).to.equal('date + date : operation unsupported for one or more operands types'); 30 | } 31 | }); 32 | 33 | it('should do time subtraction and return a duration', function() { 34 | const text = 'time("T13:10:06") - time("T13:10:05")'; 35 | const parsedGrammar = FEEL.parse(text); 36 | const result = parsedGrammar.build(); 37 | expect(result.isDuration).to.be.true; 38 | expect(result.isDtd).to.be.true; 39 | expect(result.seconds).to.equal(1); 40 | }); 41 | 42 | it('should do throw error for time addition', function() { 43 | const text = 'time("T13:10:06") + time("T13:10:05")'; 44 | const parsedGrammar = FEEL.parse(text); 45 | try { 46 | parsedGrammar.build(); 47 | fail('Expected error to be thrown'); 48 | } catch (err) { 49 | expect(err.message).to.equal('time + time : operation unsupported for one or more operands types'); 50 | } 51 | }); 52 | 53 | it('should do years and months duration subtraction and return a years and months duration', function() { 54 | const text = 'duration("P1Y13M") - duration("P1M")'; 55 | const parsedGrammar = FEEL.parse(text); 56 | const result = parsedGrammar.build(); 57 | expect(result.isDuration).to.be.true; 58 | expect(result.isYmd).to.be.true; 59 | expect(result.years).to.equal(2); 60 | }); 61 | 62 | it('should do years and months duration addition and return a years and months duration', function() { 63 | const text = 'duration("P1Y11M") + duration("P1M")'; 64 | const parsedGrammar = FEEL.parse(text); 65 | const result = parsedGrammar.build(); 66 | expect(result.isDuration).to.be.true; 67 | expect(result.isYmd).to.be.true; 68 | expect(result.years).to.equal(2); 69 | }); 70 | 71 | it('should add years and months duration addition to date and time and return a date and time', function() { 72 | const text = 'date and time("2012-12-24T23:59:00") + duration("P1Y")'; 73 | const parsedGrammar = FEEL.parse(text); 74 | const result = parsedGrammar.build(); 75 | expect(result.isDateTime).to.be.true; 76 | expect(result.year).to.equal(2013); 77 | }); 78 | 79 | it('should multiply years and months duration with number and return a years and months duration', function() { 80 | const text = 'duration("P1Y5M") * 5'; 81 | const parsedGrammar = FEEL.parse(text); 82 | const result = parsedGrammar.build(); 83 | expect(result.years).to.equal(7); 84 | expect(result.months).to.equal(1); 85 | }); 86 | 87 | it('should multiply days and time duration with number and return a days and time duration', function() { 88 | const text = 'duration("P5DT12H20M40S") * 5'; 89 | const parsedGrammar = FEEL.parse(text); 90 | const result = parsedGrammar.build(); 91 | expect(result.days).to.equal(27); 92 | expect(result.hours).to.equal(13); 93 | expect(result.minutes).to.equal(43); 94 | expect(result.seconds).to.equal(20); 95 | }); 96 | 97 | it('should multiply years and months duration with number and return a years and months duration', function() { 98 | const text = 'duration("P1Y5M") * 5'; 99 | const parsedGrammar = FEEL.parse(text); 100 | const result = parsedGrammar.build(); 101 | expect(result.years).to.equal(7); 102 | expect(result.months).to.equal(1); 103 | }); 104 | 105 | it('should divide days and time duration with number and return a null when the number is 0', function() { 106 | const text = 'duration("P5DT12H20M40S") / 0'; 107 | const parsedGrammar = FEEL.parse(text); 108 | const result = parsedGrammar.build(); 109 | expect(result).to.be.null; 110 | }); 111 | 112 | it('should divide days and time duration with number and return a days and time duration', function() { 113 | const text = 'duration("P5DT12H20M40S") / 5'; 114 | const parsedGrammar = FEEL.parse(text); 115 | const result = parsedGrammar.build(); 116 | expect(result.days).to.equal(1); 117 | expect(result.hours).to.equal(2); 118 | expect(result.minutes).to.equal(28); 119 | expect(result.seconds).to.equal(8); 120 | }); 121 | 122 | it('should divide years and months duration with number and return a null when the number is 0', function() { 123 | const text = 'duration("P1Y5M") / 0'; 124 | const parsedGrammar = FEEL.parse(text); 125 | const result = parsedGrammar.build(); 126 | expect(result).to.be.null; 127 | }); 128 | 129 | it('should divide years and months duration with number and return a years and months duration', function() { 130 | const text = 'duration("P5Y5M") / 5'; 131 | const parsedGrammar = FEEL.parse(text); 132 | const result = parsedGrammar.build(); 133 | expect(result.years).to.equal(1); 134 | expect(result.months).to.equal(1); 135 | }); 136 | 137 | it('should divide number with years and months duration and return a years and months duration', function() { 138 | const text = '5 / duration("P5Y5M")'; 139 | const parsedGrammar = FEEL.parse(text); 140 | const result = parsedGrammar.build(); 141 | expect(result.years).to.equal(1); 142 | expect(result.months).to.equal(1); 143 | }); 144 | 145 | it('should add duration to date and time with variables', function() { 146 | const text = 'date and time(dt) + duration("P" + numDays + "D")'; 147 | const parsedGrammar = FEEL.parse(text); 148 | const result = parsedGrammar.build( { dt: new Date('2018-03-01T00:00:00+01:00'), numDays: 5}); 149 | expect(result.isDateTime).to.be.true; 150 | expect(result.toISOString()).to.equal('2018-03-05T23:00:00.000Z'); 151 | }); 152 | 153 | it('should subtract days with month rollover (incl. time)', function() { 154 | const text = 'date and time("2018-07-10T05:00:00+00:00") - duration("P2Y1M10D")'; 155 | const parsedGrammar = FEEL.parse(text); 156 | const result = parsedGrammar.build(); 157 | expect(result.isDateTime).to.be.true; 158 | expect(result.toISOString()).to.equal('2016-05-31T05:00:00.000Z'); 159 | }); 160 | 161 | it('should subtract days with month rollover', function() { 162 | const text = 'date("2018-07-10") - duration("P2Y1M10D")'; 163 | const parsedGrammar = FEEL.parse(text); 164 | const result = parsedGrammar.build(); 165 | expect(result.isDate).to.be.true; 166 | expect(result.toISOString()).to.equal('2016-05-31T00:00:00.000Z'); 167 | }); 168 | 169 | it('should subtract three months from last day of month (with time) correctly', function() { 170 | const text = 'date and time("2018-07-31T00:00:00+00:00") - duration("P3M")'; 171 | const parsedGrammar = FEEL.parse(text); 172 | const result = parsedGrammar.build(); 173 | expect(result.isDateTime).to.be.true; 174 | expect(result.toISOString()).to.equal('2018-04-30T00:00:00.000Z'); 175 | }); 176 | 177 | it('should subtract three months from last day of month correctly', function() { 178 | const text = 'date("2018-07-31") - duration("P3M")'; 179 | const parsedGrammar = FEEL.parse(text); 180 | const result = parsedGrammar.build(); 181 | expect(result.isDate).to.be.true; 182 | expect(result.toISOString()).to.equal('2018-04-30T00:00:00.000Z'); 183 | }); 184 | 185 | it('should add days with month rollover (incl. time)', function() { 186 | const text = 'date and time("2016-05-21T05:00:00+00:00") + duration("P2Y1M10D")'; 187 | const parsedGrammar = FEEL.parse(text); 188 | const result = parsedGrammar.build(); 189 | expect(result.isDateTime).to.be.true; 190 | expect(result.toISOString()).to.equal('2018-07-01T05:00:00.000Z'); 191 | }); 192 | 193 | it('should add days with month rollover', function() { 194 | const text = 'date("2016-05-21") + duration("P2Y1M10D")'; 195 | const parsedGrammar = FEEL.parse(text); 196 | const result = parsedGrammar.build(); 197 | expect(result.isDate).to.be.true; 198 | expect(result.toISOString()).to.equal('2018-07-01T00:00:00.000Z'); 199 | }); 200 | 201 | it('should add two months to last day of month (with time) correctly', function() { 202 | const text = 'date and time("2018-07-31T00:00:00+00:00") + duration("P2M")'; 203 | const parsedGrammar = FEEL.parse(text); 204 | const result = parsedGrammar.build(); 205 | expect(result.isDateTime).to.be.true; 206 | expect(result.toISOString()).to.equal('2018-09-30T00:00:00.000Z'); 207 | }); 208 | 209 | it('should add two months to last day of month correctly', function() { 210 | const text = 'date("2018-07-31") + duration("P2M")'; 211 | const parsedGrammar = FEEL.parse(text); 212 | const result = parsedGrammar.build(); 213 | expect(result.isDate).to.be.true; 214 | expect(result.toISOString()).to.equal('2018-09-30T00:00:00.000Z'); 215 | }); 216 | 217 | }); 218 | -------------------------------------------------------------------------------- /test/arithmetic-expression/feel-arithmetic-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('Arithmetic expression ast parsing test'), function() { 13 | it('Successfully builds ast from simple arithmetic expression', function() { 14 | var text = 'a + b - c'; 15 | var _context = { 16 | a: 10, 17 | b: 20, 18 | c: 5 19 | }; 20 | var parsedGrammar = FEEL.parse(text); 21 | expect(parsedGrammar.build(_context)).to.equal(25); 22 | }); 23 | 24 | it('Successfully builds ast from simple arithmetic comparison', function() { 25 | var text = '< a + b'; 26 | var _context = { 27 | a: 10, 28 | b: 20, 29 | }; 30 | var parsedGrammar = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 31 | const result = parsedGrammar.build(_context); 32 | expect(result).not.to.be.undefined; 33 | expect(result(29)).to.be.true; 34 | expect(result(30)).to.be.false; 35 | }); 36 | 37 | /* this test currently fails - TODO: fix the grammar with respect to arithmetic expressions 38 | it('Successfully builds ast from arithmetic expression with correct operator precedence', function() { 39 | var text = 'a + b / c - d'; 40 | 41 | var _context = { 42 | a: 2, 43 | b: 6, 44 | c: 3, 45 | d: 1, 46 | }; 47 | 48 | var parsedGrammar = FEEL.parse(text); 49 | expect(parsedGrammar.build(_context)).to.equal(3); 50 | }); 51 | */ 52 | 53 | /* this test currently fails - TODO: fix the grammar with respect to arithmetic expressions 54 | it('Successfully builds ast from arithmetic expression', function() { 55 | var text = '((a + b)/c - (2 + e*2))**f'; 56 | 57 | var _context = { 58 | a: 10, 59 | b: 20, 60 | c: 5, 61 | d: 1, 62 | e: 3, 63 | f: 3 64 | }; 65 | 66 | var parsedGrammar = FEEL.parse(text); 67 | expect(parsedGrammar.build(_context)).to.equal(-8); 68 | }); 69 | */ 70 | 71 | it('Successfully builds ast from arithmetic expression', function() { 72 | var text = '1-(1+rate/12)**-term'; 73 | var _context = { 74 | rate: 12, 75 | term: 5 76 | }; 77 | var parsedGrammar = FEEL.parse(text); 78 | expect(parsedGrammar.build(_context)).to.equal(0.96875); 79 | }); 80 | 81 | }); 82 | -------------------------------------------------------------------------------- /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 | }); -------------------------------------------------------------------------------- /test/ast-parser-test.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | const chalk = require('chalk'); 6 | const chai = require('chai'); 7 | const expect = chai.expect; 8 | const moment = require('moment'); 9 | const FEEL = require('../dist/feel'); 10 | const builtInFns = require('../utils/built-in-functions'); 11 | 12 | describe(chalk.blue('ast parsing tests'), function() { 13 | 14 | it('ProgramNode.build', function() { 15 | const text = 'a'; 16 | const _context = { 17 | a: 42, 18 | }; 19 | const node = FEEL.parse(text, { startRule: 'Start' }); 20 | expect(node.type).to.equal('Program'); 21 | const result = node.build(_context); 22 | expect(result).to.equal(42); 23 | }); 24 | 25 | it('IntervalNode.build (open interval)', function() { 26 | const text = '(10 .. a)'; 27 | const _context = { 28 | a: 42, 29 | }; 30 | const node = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 31 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 32 | const intervalNode = node.expr[0]; 33 | expect(intervalNode.type).to.equal('Interval'); 34 | const result = intervalNode.build({ context: _context }); 35 | expect(typeof result).to.equal('function'); 36 | expect(result(10)).to.be.false; 37 | expect(result(11)).to.be.true; 38 | expect(result(41)).to.be.true; 39 | expect(result(42)).to.be.false; 40 | }); 41 | 42 | it('IntervalNode.build (closed interval)', function() { 43 | const text = '[10 .. a]'; 44 | const _context = { 45 | a: 42, 46 | }; 47 | const node = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 48 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 49 | const intervalNode = node.expr[0]; 50 | expect(intervalNode.type).to.equal('Interval'); 51 | const result = intervalNode.build({ context: _context }); 52 | expect(typeof result).to.equal('function'); 53 | expect(result(9)).to.be.false; 54 | expect(result(10)).to.be.true; 55 | expect(result(42)).to.be.true; 56 | expect(result(43)).to.be.false; 57 | }); 58 | 59 | it('SimplePositiveUnaryTestNode.build', function() { 60 | const text = '< a'; 61 | const _context = { 62 | a: 42, 63 | }; 64 | const node = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 65 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 66 | const simplePositiveUnaryTestsNode = node.expr[0]; 67 | expect(simplePositiveUnaryTestsNode.type).to.equal('SimplePositiveUnaryTest'); 68 | const result = simplePositiveUnaryTestsNode.build({ context: _context }); 69 | expect(typeof result).to.equal('function'); 70 | expect(result(41)).to.be.true; 71 | expect(result(42)).to.be.false; 72 | expect(result(43)).to.be.false; 73 | }); 74 | 75 | it('SimpleUnaryTestsNode.build', function() { 76 | const text = 'a, 10'; 77 | const _context = { 78 | a: 42, 79 | }; 80 | const node = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 81 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 82 | const result = node.build(_context); 83 | expect(typeof result).to.equal('function'); 84 | expect(result(10)).to.be.true; 85 | expect(result(42)).to.be.true; 86 | expect(result(23)).to.be.false; 87 | }); 88 | 89 | it('SimpleUnaryTestsNode.build (not)', function() { 90 | const text = 'not(a, 10)'; 91 | const _context = { 92 | a: 42, 93 | }; 94 | const node = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 95 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 96 | const result = node.build(_context); 97 | expect(typeof result).to.equal('function'); 98 | expect(result(10)).to.be.false; 99 | expect(result(42)).to.be.false; 100 | expect(result(23)).to.be.true; 101 | }); 102 | 103 | it('QualifiedNameNode.build', function() { 104 | const text = 'a'; 105 | const _context = { 106 | a: 42, 107 | }; 108 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 109 | expect(node.type).to.equal('SimpleExpressions'); 110 | const qualifiedNameNode = node.simpleExpressions[0]; 111 | expect(qualifiedNameNode.type).to.equal('QualifiedName'); 112 | const result = qualifiedNameNode.build({ context: _context }); 113 | expect(result).to.equal(42); 114 | }); 115 | 116 | it('ArithmeticExpressionNode.build', function() { 117 | const text = 'a + 1'; 118 | const _context = { 119 | a: 42, 120 | }; 121 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 122 | expect(node.type).to.equal('SimpleExpressions'); 123 | const arithmeticExpressionNode = node.simpleExpressions[0]; 124 | expect(arithmeticExpressionNode.type).to.equal('ArithmeticExpression'); 125 | const result = arithmeticExpressionNode.build({ context: _context }); 126 | expect(result).to.equal(43); 127 | }); 128 | 129 | it('SimpleExpressionsNode.build', function() { 130 | const text = '1'; 131 | const _context = { }; 132 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 133 | expect(node.type).to.equal('SimpleExpressions'); 134 | const result = node.build(_context); 135 | expect(result).to.have.ordered.members([ 1 ]); 136 | }); 137 | 138 | it('NameNode.build', function() { 139 | const text = 'a'; 140 | const _context = { 141 | a: 42, 142 | }; 143 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 144 | expect(node.type).to.equal('SimpleExpressions'); 145 | const qualifiedNameNode = node.simpleExpressions[0]; 146 | expect(qualifiedNameNode.type).to.equal('QualifiedName'); 147 | const nameNode = qualifiedNameNode.names[0]; 148 | expect(nameNode.type).to.equal('Name'); 149 | const result = nameNode.build({ context: _context }); 150 | expect(result).to.equal(42); 151 | }); 152 | 153 | it('LiteralNode.build', function() { 154 | const text = '10'; 155 | const _context = { }; 156 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 157 | expect(node.type).to.equal('SimpleExpressions'); 158 | const literalNode = node.simpleExpressions[0]; 159 | expect(literalNode.type).to.equal('Literal'); 160 | const result = literalNode.build({ context: _context }); 161 | expect(result).to.equal(10); 162 | }); 163 | 164 | it('DateTimeLiteralNode.build date from inline string', function() { 165 | const text = 'date("2017-05-01")'; 166 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 167 | expect(node.type).to.equal('SimpleExpressions'); 168 | const dateTimeLiteralNode = node.simpleExpressions[0]; 169 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 170 | const result = dateTimeLiteralNode.build({ context: Object.assign({}, {}, builtInFns) }); 171 | expect(result.utc().format('YYYY MM DD')).to.equal('2017 05 01'); 172 | }); 173 | 174 | it('DateTimeLiteralNode.build date from string', function() { 175 | const text = 'date(d)'; 176 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 177 | expect(node.type).to.equal('SimpleExpressions'); 178 | const dateTimeLiteralNode = node.simpleExpressions[0]; 179 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 180 | const result = dateTimeLiteralNode.build({ context: Object.assign({ d: '2018-03-01' }, {}, builtInFns) }); 181 | expect(result.format('YYYY MM DD HH mm SS')).to.equal('2018 03 01 00 00 00'); 182 | }); 183 | 184 | it('DateTimeLiteralNode.build date from Javascript date', function() { 185 | const text = 'date(d)'; 186 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 187 | expect(node.type).to.equal('SimpleExpressions'); 188 | const dateTimeLiteralNode = node.simpleExpressions[0]; 189 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 190 | const result = dateTimeLiteralNode.build({ context: Object.assign({}, { d: new Date('2018-03-01T00:00:00+01:00') }, builtInFns) }); 191 | expect(result.utcOffset(1).format('YYYY MM DD HH mm SS')).to.equal('2018 02 28 01 00 00'); 192 | }); 193 | 194 | it('DateTimeLiteralNode.build date from moment-js instance', function() { 195 | const text = 'date(d)'; 196 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 197 | expect(node.type).to.equal('SimpleExpressions'); 198 | const dateTimeLiteralNode = node.simpleExpressions[0]; 199 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 200 | const result = dateTimeLiteralNode.build({ context: Object.assign({}, { d: moment.parseZone('2018-03-01T00:00:00+01:00') }, builtInFns) }); 201 | expect(result.utcOffset(1).format('YYYY MM DD HH mm SS')).to.equal('2018 02 28 01 00 00'); 202 | }); 203 | 204 | it('DateTimeLiteralNode.build date and time from Javascript date', function() { 205 | const text = 'date and time(d)'; 206 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 207 | expect(node.type).to.equal('SimpleExpressions'); 208 | const dateTimeLiteralNode = node.simpleExpressions[0]; 209 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 210 | const result = dateTimeLiteralNode.build({ context: Object.assign({}, { d: new Date('2018-03-01T00:00:00+01:00') }, builtInFns) }); 211 | expect(result.utcOffset(1).format('YYYY MM DD HH mm SS')).to.equal('2018 03 01 00 00 00'); 212 | }); 213 | 214 | it('DateTimeLiteralNode.build date and time from moment-js instance', function() { 215 | const text = 'date and time(d)'; 216 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 217 | expect(node.type).to.equal('SimpleExpressions'); 218 | const dateTimeLiteralNode = node.simpleExpressions[0]; 219 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 220 | const result = dateTimeLiteralNode.build({ context: Object.assign({}, { d: moment.parseZone('2018-03-01T00:00:00+01:00') }, builtInFns) }); 221 | expect(result.utcOffset(1).format('YYYY MM DD HH mm SS')).to.equal('2018 03 01 00 00 00'); 222 | }); 223 | 224 | it('FunctionInvocationNode.build', function() { 225 | const text = 'plus(42, 1)'; 226 | const _context = { 227 | plus: (x, y) => x + y 228 | }; 229 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 230 | expect(node.type).to.equal('SimpleExpressions'); 231 | const functionInvocationNode = node.simpleExpressions[0]; 232 | expect(functionInvocationNode.type).to.equal('FunctionInvocation'); 233 | const result = functionInvocationNode.build({ context: _context }); 234 | expect(result).to.equal(43); 235 | }); 236 | 237 | it('PositionalParametersNode.build', function() { 238 | const text = 'plus(a, b)'; 239 | const _context = { 240 | plus: (x, y) => x + y, 241 | a: 42, 242 | b: 23 243 | }; 244 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 245 | expect(node.type).to.equal('SimpleExpressions'); 246 | const functionInvocationNode = node.simpleExpressions[0]; 247 | expect(functionInvocationNode.type).to.equal('FunctionInvocation'); 248 | const positionalParametersNode = functionInvocationNode.params; 249 | expect(positionalParametersNode.type).to.equal('PositionalParameters'); 250 | const result = positionalParametersNode.build({ context: _context }); 251 | expect(result).to.have.ordered.members([42, 23]); 252 | }); 253 | 254 | it('ComparisonExpressionNode.build', function() { 255 | const text = '42 != a'; 256 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 257 | expect(node.type).to.equal('SimpleExpressions'); 258 | const comparisonExpressionNode = node.simpleExpressions[0]; 259 | expect(comparisonExpressionNode.type).to.equal('ComparisonExpression'); 260 | const result = comparisonExpressionNode.build({ context: { a: 42 } }); 261 | expect(result).to.be.false; 262 | const otherResult = comparisonExpressionNode.build({ context: { a: 23 } }); 263 | expect(otherResult).to.be.true; 264 | }); 265 | }); 266 | -------------------------------------------------------------------------------- /test/comparision-expression/feel-comparision-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 | const dateTime = require('../../utils/built-in-functions/date-time-functions'); 12 | 13 | describe(chalk.blue('Comparison expression ast parsing test'), function() { 14 | 15 | it('Successfully builds ast from simple comparison', function() { 16 | var text = '<= 5'; 17 | 18 | var parsedGrammar = FEEL.parse(text); 19 | const result = parsedGrammar.build(); 20 | expect(result).not.to.be.undefined; 21 | expect(result(4)).to.be.true; 22 | expect(result(5)).to.be.true; 23 | expect(result(6)).to.be.false; 24 | }); 25 | 26 | it('Successfully builds ast from comparison', function() { 27 | var text = '(5..10]'; 28 | 29 | var parsedGrammar = FEEL.parse(text); 30 | const result = parsedGrammar.build(); 31 | expect(result).not.to.be.undefined; 32 | expect(result(4)).to.be.false; 33 | expect(result(5)).to.be.false; 34 | expect(result(6)).to.be.true; 35 | expect(result(10)).to.be.true; 36 | expect(result(11)).to.be.false; 37 | }); 38 | 39 | it('Successfully builds ast from comparison', function() { 40 | var text = '[5..10]'; 41 | 42 | var parsedGrammar = FEEL.parse(text); 43 | const result = parsedGrammar.build(); 44 | expect(result).not.to.be.undefined; 45 | expect(result(4)).to.be.false; 46 | expect(result(5)).to.be.true; 47 | expect(result(10)).to.be.true; 48 | expect(result(11)).to.be.false; 49 | }); 50 | 51 | it('Successfully builds ast from comparison', function() { 52 | var text = '4,5,6'; 53 | 54 | var parsedGrammar = FEEL.parse(text, { startRule: "SimpleUnaryTests" }); 55 | const result = parsedGrammar.build(); 56 | expect(result).not.to.be.undefined; 57 | expect(result(3)).to.be.false; 58 | expect(result(4)).to.be.true; 59 | expect(result(5)).to.be.true; 60 | expect(result(6)).to.be.true; 61 | expect(result(7)).to.be.false; 62 | }); 63 | 64 | it('Successfully builds ast from comparison', function() { 65 | var text = '<5,>5'; 66 | 67 | var parsedGrammar = FEEL.parse(text); 68 | const result = parsedGrammar.build(); 69 | expect(result).not.to.be.undefined; 70 | expect(result(4)).to.be.true; 71 | expect(result(5)).to.be.false; 72 | expect(result(6)).to.be.true; 73 | }); 74 | 75 | it('Successfully builds ast from comparison', function() { 76 | var text = '>= (7 + g)'; 77 | 78 | _context = { 79 | g: 7 80 | } 81 | var parsedGrammar = FEEL.parse(text, { startRule: "SimpleUnaryTests"}); 82 | const result = parsedGrammar.build(_context); 83 | expect(result).not.to.be.undefined; 84 | expect(result(13)).to.be.false; 85 | expect(result(14)).to.be.true; 86 | expect(result(15)).to.be.true; 87 | }); 88 | 89 | it('Successfully compare dates with ">"', function() { 90 | var text = '> date("2012-12-24")'; 91 | var parsedGrammar = FEEL.parse(text, { startRule: "SimpleUnaryTests"}); 92 | const result = parsedGrammar.build(); 93 | expect(result(dateTime.date("2012-12-25"))).to.be.true; 94 | expect(result(dateTime.date("2012-12-24"))).to.be.false; 95 | expect(result(dateTime.date("2012-12-23"))).to.be.false; 96 | }); 97 | 98 | it('Successfully compare dates with ">="', function() { 99 | var text = '>= date("2012-12-24")'; 100 | var parsedGrammar = FEEL.parse(text); 101 | const result = parsedGrammar.build(); 102 | expect(result(dateTime.date("2012-12-25"))).to.be.true; 103 | expect(result(dateTime.date("2012-12-24"))).to.be.true; 104 | expect(result(dateTime.date("2012-12-23"))).to.be.false; 105 | }); 106 | 107 | it('Successfully compare string "<"', function() { 108 | var text = '< "ABC"'; 109 | var parsedGrammar = FEEL.parse(text); 110 | const result = parsedGrammar.build(); 111 | expect(result("ABC")).to.be.false; 112 | expect(result("BBC")).to.be.false; 113 | expect(result("AAB")).to.be.true; 114 | }); 115 | 116 | it('Successfully compare string "<="', function() { 117 | var text = '<= "ABC"'; 118 | var parsedGrammar = FEEL.parse(text); 119 | const result = parsedGrammar.build(); 120 | expect(result("ABC")).to.be.true; 121 | expect(result("BBC")).to.be.false; 122 | expect(result("AAB")).to.be.true; 123 | }); 124 | 125 | it('Successfully compare string ">"', function() { 126 | var text = '> "ABC"'; 127 | var parsedGrammar = FEEL.parse(text); 128 | const result = parsedGrammar.build(); 129 | expect(result("ABC")).to.be.false; 130 | expect(result("BBC")).to.be.true; 131 | expect(result("AAB")).to.be.false; 132 | }); 133 | 134 | it('Successfully compare string ">="', function() { 135 | var text = '>= "ABC"'; 136 | var parsedGrammar = FEEL.parse(text); 137 | const result = parsedGrammar.build(); 138 | expect(result("ABC")).to.be.true; 139 | expect(result("BBC")).to.be.true; 140 | expect(result("AAB")).to.be.false; 141 | }); 142 | 143 | it('Successfully compare string "="', function() { 144 | var text = '"XYZ"'; 145 | var parsedGrammar = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 146 | const result = parsedGrammar.build(); 147 | expect(result("XYZ")).to.be.true; 148 | expect(result("ABC")).to.be.false; 149 | }); 150 | 151 | it('Successfully compare string "!="', function() { 152 | var text = 'not("XYZ")'; 153 | var parsedGrammar = FEEL.parse(text); 154 | const result = parsedGrammar.build(); 155 | expect(result("XYZ")).to.be.false; 156 | expect(result("ABC")).to.be.true; 157 | }); 158 | 159 | it('Successfully compare date and time with "<"', function() { 160 | var text = '< date and time("2012-12-25T00:00:00")'; 161 | var parsedGrammar = FEEL.parse(text); 162 | const result = parsedGrammar.build(); 163 | expect(result(dateTime['date and time']("2012-12-24T23:59:59"))).to.be.true; 164 | expect(result(dateTime['date and time']("2012-12-25T00:00:00"))).to.be.false; 165 | expect(result(dateTime['date and time']("2012-12-25T00:00:01"))).to.be.false; 166 | }); 167 | 168 | it('Successfully compare date with "<"', function() { 169 | var text = '< date("2012-12-24")'; 170 | var parsedGrammar = FEEL.parse(text); 171 | const result = parsedGrammar.build(); 172 | expect(result(dateTime.date("2012-12-25"))).to.be.false; 173 | expect(result(dateTime.date("2012-12-24"))).to.be.false; 174 | expect(result(dateTime.date("2012-12-23"))).to.be.true; 175 | expect(result(dateTime.date("2013-11-23"))).to.be.false; 176 | }); 177 | 178 | it('Successfully compare date with duration added', function() { 179 | var text = '< date(date("2012-12-24") + duration("P1D"))'; 180 | var parsedGrammar = FEEL.parse(text); 181 | const result = parsedGrammar.build(); 182 | expect(result(dateTime.date("2012-12-26"))).to.be.false; 183 | expect(result(dateTime.date("2012-12-25"))).to.be.false; 184 | expect(result(dateTime.date("2012-12-24"))).to.be.true; 185 | expect(result(dateTime.date("2012-12-23"))).to.be.true; 186 | }); 187 | 188 | it('Successfully compare time with "<"', function() { 189 | var text = '< time("T23:59:00Z")'; 190 | var parsedGrammar = FEEL.parse(text); 191 | const result = parsedGrammar.build(); 192 | expect(result(dateTime.time("T23:58:59Z"))).to.be.true; 193 | expect(result(dateTime.time("T23:59:00Z"))).to.be.false; 194 | expect(result(dateTime.time("T23:59:01Z"))).to.be.false; 195 | }); 196 | 197 | it('Successfully compare days and time duration with "<"', function() { 198 | var text = '< duration("P2D")'; 199 | var parsedGrammar = FEEL.parse(text); 200 | const result = parsedGrammar.build(); 201 | expect(result(dateTime.duration("P1D"))).to.be.true; 202 | expect(result(dateTime.duration("P2D"))).to.be.false; 203 | expect(result(dateTime.duration("P3D"))).to.be.false; 204 | }); 205 | 206 | it('Successfully compare years and months duration with "<"', function() { 207 | var text = '< duration("P26M")'; 208 | var parsedGrammar = FEEL.parse(text); 209 | const result = parsedGrammar.build(); 210 | expect(result(dateTime.duration("P2Y"))).to.be.true; 211 | expect(result(dateTime.duration("P3Y"))).to.be.false; 212 | }); 213 | 214 | it('Successfully compare date and time with "<="', function() { 215 | var text = '<= date and time("2012-12-25T00:00:00")'; 216 | var parsedGrammar = FEEL.parse(text); 217 | const result = parsedGrammar.build(); 218 | expect(result(dateTime['date and time']("2012-12-24T23:59:59"))).to.be.true; 219 | expect(result(dateTime['date and time']("2012-12-25T00:00:00"))).to.be.true; 220 | expect(result(dateTime['date and time']("2012-12-25T00:00:01"))).to.be.false; 221 | }); 222 | 223 | it('Successfully compare time successfully with ">" accross time zones', function() { 224 | var text = '> time("T12:59:00+06:30")'; 225 | var parsedGrammar = FEEL.parse(text); 226 | const result = parsedGrammar.build(); 227 | expect(result(dateTime.time("T12:59:00+05:30"))).to.be.true; 228 | expect(result(dateTime.time("T12:59:00+07:30"))).to.be.false; 229 | }); 230 | 231 | it('Successfully compare time successfully with ">=" accross time zones', function() { 232 | var text = '>= time("T12:59:00+06:30")'; 233 | var parsedGrammar = FEEL.parse(text); 234 | const result = parsedGrammar.build(); 235 | expect(result(dateTime.time("T12:59:00+05:30"))).to.be.true; 236 | expect(result(dateTime.time("T12:59:00+06:30"))).to.be.true; 237 | expect(result(dateTime.time("T12:59:00+07:30"))).to.be.false; 238 | }); 239 | 240 | it('Successfully compare date successfully with ">"', function() { 241 | var text = '> date("2011-10-09")'; 242 | var parsedGrammar = FEEL.parse(text); 243 | const result = parsedGrammar.build(); 244 | expect(result(dateTime.date("2011-10-10"))).to.be.true; 245 | expect(result(dateTime.date("2011-10-09"))).to.be.false; 246 | expect(result(dateTime.date("2011-10-08"))).to.be.false; 247 | expect(result(dateTime.date("2010-11-10"))).to.be.false; 248 | }); 249 | 250 | it('Successfully compare date successfully with ">="', function() { 251 | var text = '>= date("2011-10-09")'; 252 | var parsedGrammar = FEEL.parse(text); 253 | const result = parsedGrammar.build(); 254 | expect(result(dateTime.date("2011-10-10"))).to.be.true; 255 | expect(result(dateTime.date("2011-10-09"))).to.be.true; 256 | expect(result(dateTime.date("2011-10-08"))).to.be.false; 257 | }); 258 | 259 | it('Successfully parse and build equality expression using date with multiple args and date with string arg', function() { 260 | var text = 'date("2012-12-25")'; 261 | var parsedGrammar = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 262 | const result = parsedGrammar.build(); 263 | expect(result(dateTime.date(2012, 11, 25))).to.be.true; 264 | }); 265 | 266 | it('Successfully parse and build equality expression using durations', function() { 267 | var text = 'duration("P26M")'; 268 | var parsedGrammar = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 269 | const result = parsedGrammar.build(); 270 | expect(result(dateTime.duration("P2Y2M"))).to.be.true; 271 | expect(result(dateTime.duration("P2Y1M"))).to.be.false; 272 | }); 273 | 274 | it('Successfully parse and build equality expression using date and time in a "in" comparison expression', function() { 275 | var text = '[date and time("2017-04-12T11:30:00Z")..date and time("2017-04-12T12:45:00Z")]'; 276 | var parsedGrammar = FEEL.parse(text); 277 | const result = parsedGrammar.build(); 278 | expect(result(dateTime['date and time']("2017-04-12T12:45:00Z"))).to.be.true; 279 | expect(result(dateTime['date and time']("2017-04-12T11:30:00Z"))).to.be.true; 280 | expect(result(dateTime['date and time']("2017-04-12T11:29:00Z"))).to.be.false; 281 | expect(result(dateTime['date and time']("2017-04-12T12:46:00Z"))).to.be.false; 282 | }); 283 | 284 | it('Successfully parse and build function evaluation with parameters', function() { 285 | var text = 'add(a, b.c)'; 286 | var parsedGrammar = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 287 | const result = parsedGrammar.build({ add: (v1, v2) => v1 + v2, a: 4, b: { c: 5 }}); 288 | expect(result(9)).to.be.true; 289 | expect(result(10)).to.be.false; 290 | }); 291 | 292 | it('Successfully parse and build function evaluation with arithmetic expressions', function() { 293 | var text = 'add(2 + 2, (4 + 6) / 2)'; 294 | var parsedGrammar = FEEL.parse(text, { startRule: 'SimpleUnaryTests' }); 295 | const result = parsedGrammar.build({ add: (v1, v2) => v1 + v2}); 296 | expect(result(9)).to.be.true; 297 | expect(result(10)).to.be.false; 298 | }); 299 | 300 | }); 301 | -------------------------------------------------------------------------------- /test/data/test-collect-drg.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | output1.length 17 | 18 | 19 | 20 | output2.nested.length 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 100 31 | 32 | 33 | 34 | 35 | 2 36 | 37 | 2 38 | 39 | 75 40 | 41 | 42 | 43 | 44 | 3 45 | 46 | 2 47 | 48 | 50 49 | 50 | 51 | 52 | 53 | 3 54 | 55 | 3 56 | 57 | 25 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 0 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | input.category 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /test/data/test-collect.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.category 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /test/data/test-decode-special-characters.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | reputationValue 8 | 9 | 10 | 11 | 12 | 13 | < 50 14 | 15 | 16 | "bad" 17 | 18 | 19 | 20 | 21 | > 90 22 | 23 | 24 | "good" 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | "so-so" 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/data/test-empty-decision.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /test/data/test-empty-input.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | "d'oh" 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /test/data/test-input-expression.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.score + 1 7 | 8 | 9 | 10 | 11 | 1 12 | 13 | 14 | 15 | 16 | 17 | 2 18 | 19 | 20 | 21 | 22 | 23 | 24 | 3 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/data/test-input-variable.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | issue.id 8 | 9 | 10 | 11 | 12 | This will not work out of the box in most S-FEEL engines. 13 | 14 | starts with(issue.id, "CAM"), "CAMUNDA" 15 | 16 | 17 | "Camunda" 18 | 19 | 20 | 21 | 22 | "HIBERNATE" 23 | 24 | 25 | "Hibernate" 26 | 27 | 28 | 29 | 30 | upper case("drools") 31 | 32 | 33 | "Drools" 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /test/data/test-no-matching-rule.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.category 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/test-no-matching-rules.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | input1 10 | 11 | 12 | 13 | input2 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | input1 28 | 29 | 30 | 31 | input2 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /test/data/test-rule-order.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.category 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/data/test-type-error.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | desiredDish 29 | 30 | 31 | 32 | 33 | guestsWithChildren 34 | 35 | 36 | 37 | 38 | Tough Stuff 39 | 40 | "Spareribs" 41 | 42 | 43 | true 44 | 45 | 46 | "Aecht Schlenkerla Rauchbier" 47 | 48 | 49 | 50 | 51 | "Stew" 52 | 53 | 54 | true 55 | 56 | 57 | "Guiness" 58 | 59 | 60 | 61 | 62 | "Roastbeef" 63 | 64 | 65 | true 66 | 67 | 68 | "Bordeaux" 69 | 70 | 71 | 72 | 73 | "Steak","Dry Aged Gourmet Steak","Light Salad and a nice Steak" 74 | 75 | 76 | true 77 | 78 | 79 | "Pinot Noir" 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | true 88 | 89 | 90 | "Apple Juice" 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | false 99 | 100 | 101 | "Water" 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | season 135 | 136 | 137 | 138 | 139 | guestCount 140 | 141 | 142 | 143 | 144 | Default value 145 | 146 | not("Fall", "Winter", "Spring", "Summer") 147 | 148 | 149 | >= 0 150 | 151 | 152 | "Instant Soup" 153 | 154 | 155 | 156 | 157 | "Fall" 158 | 159 | 160 | <= 8 161 | 162 | 163 | "Spareribs" 164 | 165 | 166 | 167 | 168 | "Winter" 169 | 170 | 171 | <= 8 172 | 173 | 174 | "Roastbeef" 175 | 176 | 177 | 178 | 179 | "Spring" 180 | 181 | 182 | <= 4 183 | 184 | 185 | "Dry Aged Gourmet Steak" 186 | 187 | 188 | 189 | Save money 190 | 191 | "Spring" 192 | 193 | 194 | [5..8] 195 | 196 | 197 | "Steak" 198 | 199 | 200 | 201 | Less effort 202 | 203 | "Fall","Winter","Spring" 204 | 205 | 206 | > 8 207 | 208 | 209 | "Stew" 210 | 211 | 212 | 213 | Hey, why not? 214 | 215 | "Summer" 216 | 217 | 218 | 219 | 220 | 221 | "Light Salad and a nice Steak" 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /test/data/test-undefined-input-entry.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.score 7 | 8 | 9 | 10 | 11 | input.highScore 12 | 13 | input.category1 14 | 15 | 16 | 17 | input.lowScore 18 | 19 | input.category2 20 | 21 | 22 | 23 | 24 | 25 | input.otherCategory 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /test/data/test-undefined-output-expression-collect.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.score 7 | 8 | 9 | 10 | 11 | 1]]> 12 | 13 | input.category1 14 | 15 | 16 | 17 | 2]]> 18 | 19 | input.category2 20 | 21 | 22 | 23 | 24 | 3]]> 25 | 26 | input.category3 27 | 28 | 29 | 30 | 31 | 32 | input.otherCategory 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/data/test-undefined-output-expression.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.score 7 | 8 | 9 | 10 | 11 | 1 12 | 13 | input.category1 14 | 15 | 16 | 17 | 2 18 | 19 | input.category2 20 | 21 | 22 | 23 | 24 | 3 25 | 26 | input.category3 27 | 28 | 29 | 30 | 31 | 32 | input.otherCategory 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /test/data/test-unique.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | input.category 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /test/data/test.dmn: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | date(input.testDate) 17 | 18 | 19 | 20 | 21 | [periodBegin .. periodBegin + periodDuration] 22 | 23 | 100 24 | 25 | 26 | 27 | 28 | 29 | 50 30 | 31 | 32 | 33 | 34 | 35 | 0 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | input.category 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | date(input.referenceDate) 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | date(input.referenceDate) 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | date(input.referenceDate) 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | date(input.referenceDate) 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | -------------------------------------------------------------------------------- /test/decision-table-xml.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | const fs = require('fs'); 6 | const chalk = require('chalk'); 7 | const chai = require('chai'); 8 | const moment = require('moment'); 9 | const expect = chai.expect; 10 | const assert = chai.assert; 11 | 12 | const { decisionTable, dateTime } = require('../index'); 13 | 14 | function readFile(filename) { 15 | return fs.readFileSync(filename, { encoding: 'UTF-8' }); 16 | } 17 | 18 | describe(chalk.blue('Parse and evaluate decision tables'), function() { 19 | 20 | it('Parse DRG', function(done) { 21 | decisionTable.parseDmnXml(readFile("./test/data/test.dmn")).then(decisions => { 22 | expect(decisions).not.to.be.undefined; 23 | expect(decisions['decisionPrimary']).not.to.be.undefined; 24 | expect(decisions['decisionDependent']).not.to.be.undefined; 25 | expect(decisions['decisionUnknown']).to.be.undefined; 26 | done(); 27 | }).catch(err => done(err)); 28 | }); 29 | 30 | it('Parse decision table', function(done) { 31 | decisionTable.parseDmnXml(readFile("./test/data/test-type-error.dmn")).then(decisions => { 32 | expect(decisions).not.to.be.undefined; 33 | done(); 34 | }).catch(err => done(err)); 35 | }); 36 | 37 | it('Evaluation decision table', function(done) { 38 | decisionTable.parseDmnXml(readFile("./test/data/test.dmn")).then(decisions => { 39 | expect(decisions['decisionDependent']).not.to.be.undefined; 40 | const context = { 41 | input: { 42 | category: "E", 43 | referenceDate: new Date("2018-01-04T00:00:00+00:00"), 44 | } 45 | }; 46 | const data = decisionTable.evaluateDecision('decisionDependent', decisions, context); 47 | expect(moment.isMoment(data.periodBegin)).to.be.true; 48 | expect(data.periodBegin.isSame(dateTime.date("2018-01-04"))).to.be.true; 49 | expect(moment.isDuration(data.periodDuration)).to.be.true; 50 | expect(data.periodDuration.months).to.equal(3); 51 | done(); 52 | }).catch(err => done(err)); 53 | }); 54 | 55 | it('Evaluation decision table with no matching rules', function(done) { 56 | decisionTable.parseDmnXml(readFile("./test/data/test-no-matching-rules.dmn")).then(decisions => { 57 | expect(decisions['decisionUnique']).not.to.be.undefined; 58 | expect(decisions['decisionCollect']).not.to.be.undefined; 59 | const context = { 60 | input: { 61 | input1: 1, 62 | input2: 2, 63 | } 64 | }; 65 | let data = decisionTable.evaluateDecision('decisionUnique', decisions, context); 66 | expect(data).not.to.be.undefined; 67 | expect(data.output1).not.to.be.undefined; 68 | expect(data.output1.nested).to.be.undefined; 69 | expect(data.output2).to.be.undefined; 70 | data = decisionTable.evaluateDecision('decisionCollect', decisions, context); 71 | expect(data).not.to.be.undefined; 72 | expect(data).to.have.ordered.members([]); 73 | done(); 74 | }).catch(err => done(err)); 75 | }); 76 | 77 | it('Evaluation decision table with required decision', function(done) { 78 | decisionTable.parseDmnXml(readFile("./test/data/test.dmn")).then(decisions => { 79 | expect(decisions['decisionPrimary']).not.to.be.undefined; 80 | const context = { 81 | input: { 82 | category: "E", 83 | referenceDate: new Date("2018-01-04T00:00:00+00:00"), 84 | testDate: new Date("2018-01-03T00:00:00+00:00"), 85 | } 86 | }; 87 | let data = decisionTable.evaluateDecision('decisionPrimary', decisions, context); 88 | expect(data.output.score).to.equal(50); 89 | context.input.testDate = new Date("2018-04-04T00:00:00+00:00"); 90 | data = decisionTable.evaluateDecision('decisionPrimary', decisions, context); 91 | expect(data.output.score).to.equal(100); 92 | context.input.testDate = new Date("2018-04-05T00:00:00+00:00"); 93 | data = decisionTable.evaluateDecision('decisionPrimary', decisions, context); 94 | expect(data.output.score).to.equal(0); 95 | done(); 96 | }).catch(err => done(err)); 97 | }); 98 | 99 | it('Evaluation decision table with hit policy COLLECT', function(done) { 100 | decisionTable.parseDmnXml(readFile("./test/data/test-collect.dmn")).then(decisions => { 101 | expect(decisions['decision']).not.to.be.undefined; 102 | const context = { 103 | input: { 104 | category: "A", 105 | } 106 | }; 107 | const data = decisionTable.evaluateDecision('decision', decisions, context); 108 | assert.deepEqual(data, [ 109 | { message: 'Message 1', output: { property: 'Value 1' }}, 110 | { message: 'Message 3', output: { property: undefined }}, 111 | { message: undefined, output: { property: 'Value 4' }}, 112 | { message: 'Message 5', output: { property: 'Value 5' }}, 113 | ]); 114 | done(); 115 | }).catch(err => done(err)); 116 | }); 117 | 118 | it('Evaluation decision table with required decision of hit policy COLLECT', function(done) { 119 | decisionTable.parseDmnXml(readFile("./test/data/test-collect-drg.dmn")).then(decisions => { 120 | expect(decisions['decisionPrimary']).not.to.be.undefined; 121 | const context = { 122 | input: { 123 | category: "A", 124 | } 125 | }; 126 | const data = decisionTable.evaluateDecision('decisionPrimary', decisions, context); 127 | expect(data.output.score).to.equal(50); 128 | done(); 129 | }).catch(err => done(err)); 130 | }); 131 | 132 | it('Evaluation decision table with hit policy RULE ORDER', function(done) { 133 | decisionTable.parseDmnXml(readFile("./test/data/test-rule-order.dmn")).then(decisions => { 134 | expect(decisions['decision']).not.to.be.undefined; 135 | const context = { 136 | input: { 137 | category: "A", 138 | } 139 | }; 140 | const data = decisionTable.evaluateDecision('decision', decisions, context); 141 | assert.deepEqual(data, [ 142 | { message: 'Message 1' }, 143 | { message: 'Message 3' }, 144 | { message: 'Message 4' }, 145 | { message: 'Message 5' }, 146 | ]); 147 | done(); 148 | }).catch(err => done(err)); 149 | }); 150 | 151 | it('Evaluation decision table with hit policy UNIQUE', function(done) { 152 | decisionTable.parseDmnXml(readFile("./test/data/test-unique.dmn")).then(decisions => { 153 | expect(decisions['decision']).not.to.be.undefined; 154 | const context = { 155 | input: { 156 | category: "B", 157 | } 158 | }; 159 | const data = decisionTable.evaluateDecision('decision', decisions, context); 160 | expect(data).not.to.be.undefined; 161 | expect(data.message).to.equal('Message 2'); 162 | done(); 163 | }).catch(err => done(err)); 164 | }); 165 | 166 | it('Return undefined if no rule matches', function(done) { 167 | decisionTable.parseDmnXml(readFile("./test/data/test-no-matching-rule.dmn")).then(decisions => { 168 | expect(decisions['decision']).not.to.be.undefined; 169 | const context = { 170 | input: { 171 | category: "D", 172 | } 173 | }; 174 | const data = decisionTable.evaluateDecision('decision', decisions, context); 175 | expect(data).not.to.be.undefined; 176 | expect(data.message).to.be.undefined; 177 | done(); 178 | }).catch(err => done(err)); 179 | }); 180 | 181 | 182 | it('Enforce uniqueness for hit policy UNIQUE', function(done) { 183 | decisionTable.parseDmnXml(readFile("./test/data/test-unique.dmn")).then(decisions => { 184 | expect(decisions['decision']).not.to.be.undefined; 185 | const context = { 186 | input: { 187 | category: "A", 188 | } 189 | }; 190 | try { 191 | decisionTable.evaluateDecision('decision', decisions, context); 192 | assert.fail(0, 1, "Uniqueness not enforced"); 193 | } catch (err) { 194 | expect(err.message).to.equal(`Decision "decision" is not unique but hit policy is UNIQUE.`); 195 | done(); 196 | } 197 | }).catch(err => done(err)); 198 | }); 199 | 200 | it('Evaluation decision with arithmetic input expression', function(done) { 201 | decisionTable.parseDmnXml(readFile("./test/data/test-input-expression.dmn")).then(decisions => { 202 | expect(decisions['decision']).not.to.be.undefined; 203 | const context = { 204 | input: { 205 | score: 1, 206 | } 207 | }; 208 | const data = decisionTable.evaluateDecision('decision', decisions, context); 209 | expect(data).not.to.be.undefined; 210 | expect(data.message).to.equal('Score 2'); 211 | done(); 212 | }).catch(err => done(err)); 213 | }); 214 | 215 | it('Evaluation decision with input expression resolving to undefined', function(done) { 216 | decisionTable.parseDmnXml(readFile("./test/data/test-input-expression.dmn")).then(decisions => { 217 | expect(decisions['decision']).not.to.be.undefined; 218 | let data = decisionTable.evaluateDecision('decision', decisions, { }); 219 | expect(data).not.to.be.undefined; 220 | expect(data.message).to.equal('other score'); 221 | data = decisionTable.evaluateDecision('decision', decisions, { input: { } }); 222 | expect(data).not.to.be.undefined; 223 | expect(data.message).to.equal('other score'); 224 | done(); 225 | }).catch(err => done(err)); 226 | }); 227 | 228 | it('Evaluation decision with input entry resolving to undefined', function(done) { 229 | decisionTable.parseDmnXml(readFile("./test/data/test-undefined-input-entry.dmn")).then(decisions => { 230 | expect(decisions['decision']).not.to.be.undefined; 231 | let data = decisionTable.evaluateDecision('decision', decisions, { input: { score: 42, otherCategory: 'poor' } }); 232 | expect(data).not.to.be.undefined; 233 | expect(data.output.category).to.equal('poor'); 234 | done(); 235 | }).catch(err => done(err)); 236 | }); 237 | 238 | it('Evaluation decision with output expression resolving to undefined with hit policy UNIQUE', function(done) { 239 | decisionTable.parseDmnXml(readFile("./test/data/test-undefined-output-expression.dmn")).then(decisions => { 240 | expect(decisions['decision']).not.to.be.undefined; 241 | data = decisionTable.evaluateDecision('decision', decisions, { input: { score: 1 } }); 242 | expect(data).not.to.be.undefined; 243 | expect(data.output).not.to.be.undefined; 244 | expect(data.output.categeory).to.be.undefined; 245 | done(); 246 | }).catch(err => done(err)); 247 | }); 248 | 249 | it('Evaluation decision with output expression resolving to undefined with hit policy COLLECT', function(done) { 250 | decisionTable.parseDmnXml(readFile("./test/data/test-undefined-output-expression-collect.dmn")).then(decisions => { 251 | expect(decisions['decision']).not.to.be.undefined; 252 | data = decisionTable.evaluateDecision('decision', decisions, { input: { score: 3, category1: 'cat1', category3: 'cat3', otherCategory: 'other' } }); 253 | assert.deepEqual(data, [ 254 | { output: { categories: 'cat1' } }, 255 | { output: { categories: undefined } }, 256 | { output: { categories: 'other' } }, 257 | ]); 258 | done(); 259 | }).catch(err => done(err)); 260 | }); 261 | 262 | it('Evaluation decision table with special characters in text node', function(done) { 263 | decisionTable.parseDmnXml(readFile("./test/data/test-decode-special-characters.dmn")).then(decisions => { 264 | expect(decisions['test_decision']).not.to.be.undefined; 265 | const context = { 266 | reputationValue: 95, 267 | }; 268 | const data = decisionTable.evaluateDecision('test_decision', decisions, context); 269 | expect(data.reputationText).to.equal('good'); 270 | done(); 271 | }).catch(err => done(err)); 272 | }); 273 | 274 | it('Evaluation decision table with input variable for functions', function(done) { 275 | decisionTable.parseDmnXml(readFile("./test/data/test-input-variable.dmn")).then(decisions => { 276 | expect(decisions['decision']).not.to.be.undefined; 277 | let data = decisionTable.evaluateDecision('decision', decisions, { issue: { id: "CAM-1234" }}); 278 | expect(data.projectName).to.equal('Camunda'); 279 | data = decisionTable.evaluateDecision('decision', decisions, { issue: { id: "CAMUNDA" }}); 280 | expect(data.projectName).to.equal('Camunda'); 281 | data = decisionTable.evaluateDecision('decision', decisions, { issue: { id: "HIBERNATE" }}); 282 | expect(data.projectName).to.equal('Hibernate'); 283 | data = decisionTable.evaluateDecision('decision', decisions, { issue: { id: "DROOLS" }}); 284 | expect(data.projectName).to.equal('Drools'); 285 | done(); 286 | }).catch(err => done(err)); 287 | }); 288 | 289 | it('Evaluation decision table with no input', function(done) { 290 | decisionTable.parseDmnXml(readFile("./test/data/test-empty-input.dmn")).then(decisions => { 291 | expect(decisions['noInput']).not.to.be.undefined; 292 | let data = decisionTable.evaluateDecision('noInput', decisions, { }); 293 | expect(data.output).to.equal("d'oh"); 294 | done(); 295 | }).catch(err => done(err)); 296 | }); 297 | 298 | it('Evaluation decision table with no input and no rules', function(done) { 299 | decisionTable.parseDmnXml(readFile("./test/data/test-empty-decision.dmn")).then(decisions => { 300 | expect(decisions['noDecision']).not.to.be.undefined; 301 | let data = decisionTable.evaluateDecision('noDecision', decisions, { }); 302 | expect(data.output).to.be.undefined; 303 | done(); 304 | }).catch(err => done(err)); 305 | }); 306 | }); 307 | -------------------------------------------------------------------------------- /test/feel-miscellaneous-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 | const chalk = require('chalk'); 8 | const chai = require('chai'); 9 | const expect = chai.expect; 10 | const FEEL = require('../dist/feel'); 11 | const dateTime = require('../utils/built-in-functions/date-time-functions'); 12 | 13 | describe(chalk.blue('Random list of rules'), function () { 14 | 15 | it('Successfully parses and executes time with offset specified as negative duration', function () { 16 | const text = 'time(10, 15, 0, -duration("PT7H"))'; 17 | const parsedText = FEEL.parse(text, { startRule: 'SimpleUnaryTests'}); 18 | expect(parsedText.build()(dateTime.time("T10:15:00-07:00"))).to.equal(true); 19 | }); 20 | 21 | it ('Successfully parses and executes SimpleExpressions and usage of mutiple grammar entry points', function () { 22 | const text = '1,2,3,4' 23 | const parsedText = FEEL.parse(text,{startRule : "SimpleExpressions"}); // parsed with "SimpleExpressions" entry point 24 | expect(parsedText.build()).to.eql([1,2,3,4]); 25 | }); 26 | 27 | }); 28 | -------------------------------------------------------------------------------- /test/handle-undefined-values.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | const chalk = require('chalk'); 6 | const chai = require('chai'); 7 | const expect = chai.expect; 8 | const FEEL = require('../dist/feel'); 9 | const builtInFns = require('../utils/built-in-functions'); 10 | 11 | describe(chalk.blue('handle undefined values tests'), function() { 12 | 13 | it('ProgramNode', function() { 14 | const node = FEEL.parse('a', { startRule: 'Start' }); 15 | expect(node.type).to.equal('Program'); 16 | let result = node.build({ a: undefined }); 17 | expect(result).to.be.undefined; 18 | result = node.build({ }); 19 | expect(result).to.be.undefined; 20 | }); 21 | 22 | it('IntervalNode', function() { 23 | const node = FEEL.parse('[10 .. a)', { startRule: 'SimpleUnaryTests' }); 24 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 25 | const intervalNode = node.expr[0]; 26 | expect(intervalNode.type).to.equal('Interval'); 27 | let result = intervalNode.build({ context: { a: undefined } }); 28 | expect(typeof result).to.equal('function'); 29 | expect(result(9)).to.be.undefined; 30 | expect(result(10)).to.be.undefined; 31 | result = intervalNode.build({ context: { } }); 32 | expect(typeof result).to.equal('function'); 33 | expect(result(9)).to.be.undefined; 34 | expect(result(10)).to.be.undefined; 35 | }); 36 | 37 | it('SimplePositiveUnaryTestNode', function() { 38 | const node = FEEL.parse('< a', { startRule: 'SimpleUnaryTests' }); 39 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 40 | const simplePositiveUnaryTestsNode = node.expr[0]; 41 | expect(simplePositiveUnaryTestsNode.type).to.equal('SimplePositiveUnaryTest'); 42 | let result = simplePositiveUnaryTestsNode.build({ context: { a: undefined} }); 43 | expect(typeof result).to.equal('function'); 44 | expect(result(41)).to.be.undefined; 45 | result = simplePositiveUnaryTestsNode.build({ context: { } }); 46 | expect(typeof result).to.equal('function'); 47 | expect(result(41)).to.be.undefined; 48 | }); 49 | 50 | it('SimpleUnaryTestsNode', function() { 51 | const node = FEEL.parse('a, 10', { startRule: 'SimpleUnaryTests' }); 52 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 53 | let result = node.build({}); 54 | expect(typeof result).to.equal('function'); 55 | expect(result(10)).to.be.true; 56 | expect(result(42)).to.be.undefined; 57 | expect(result(undefined)).to.be.undefined; 58 | result = node.build({ a: undefined }); 59 | expect(typeof result).to.equal('function'); 60 | expect(result(10)).to.be.true; 61 | expect(result(42)).to.be.undefined; 62 | expect(result(undefined)).to.be.undefined; 63 | }); 64 | 65 | it('SimpleUnaryTestsNode (not)', function() { 66 | const node = FEEL.parse('not(a, 10)', { startRule: 'SimpleUnaryTests' }); 67 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 68 | let result = node.build({ a: undefined }); 69 | expect(typeof result).to.equal('function'); 70 | expect(result(10)).to.be.false; 71 | expect(result(42)).to.be.undefined; 72 | expect(result(undefined)).to.be.undefined; 73 | result = node.build({ }); 74 | expect(typeof result).to.equal('function'); 75 | expect(result(10)).to.be.false; 76 | expect(result(42)).to.be.undefined; 77 | expect(result(undefined)).to.be.undefined; 78 | }); 79 | 80 | it('SimpleUnaryTestsNode (null)', function() { 81 | const node = FEEL.parse('null', { startRule: 'SimpleUnaryTests' }); 82 | expect(node.type).to.equal('SimpleUnaryTestsNode'); 83 | let result = node.build({}); 84 | expect(typeof result).to.equal('function'); 85 | expect(result(undefined)).to.be.undefined; 86 | expect(result(null)).to.be.true; 87 | expect(result(42)).to.be.false; 88 | }); 89 | 90 | it('QualifiedNameNode', function() { 91 | const node = FEEL.parse('a.b', { startRule: 'SimpleExpressions' }); 92 | expect(node.type).to.equal('SimpleExpressions'); 93 | const qualifiedNameNode = node.simpleExpressions[0]; 94 | expect(qualifiedNameNode.type).to.equal('QualifiedName'); 95 | let result = qualifiedNameNode.build({ context: { } }); 96 | expect(result).to.equal(undefined); 97 | result = qualifiedNameNode.build({ context: { a: undefined } }); 98 | expect(result).to.equal(undefined); 99 | result = qualifiedNameNode.build({ context: { a: { } } }); 100 | expect(result).to.equal(undefined); 101 | result = qualifiedNameNode.build({ context: { a: { b: undefined } } }); 102 | expect(result).to.equal(undefined); 103 | }); 104 | 105 | it('ArithmeticExpressionNode', function() { 106 | const node = FEEL.parse('a + 1', { startRule: 'SimpleExpressions' }); 107 | expect(node.type).to.equal('SimpleExpressions'); 108 | const arithmeticExpressionNode = node.simpleExpressions[0]; 109 | expect(arithmeticExpressionNode.type).to.equal('ArithmeticExpression'); 110 | let result = arithmeticExpressionNode.build({ context: { } }); 111 | expect(result).to.equal(undefined); 112 | result = arithmeticExpressionNode.build({ context: { a: undefined } }); 113 | expect(result).to.equal(undefined); 114 | }); 115 | 116 | it('NameNode', function() { 117 | const node = FEEL.parse('a', { startRule: 'SimpleExpressions' }); 118 | expect(node.type).to.equal('SimpleExpressions'); 119 | const qualifiedNameNode = node.simpleExpressions[0]; 120 | expect(qualifiedNameNode.type).to.equal('QualifiedName'); 121 | const nameNode = qualifiedNameNode.names[0]; 122 | expect(nameNode.type).to.equal('Name'); 123 | let result = nameNode.build({ context: { } }); 124 | expect(result).to.equal(undefined); 125 | result = nameNode.build({ context: { a: undefined } }); 126 | expect(result).to.equal(undefined); 127 | }); 128 | 129 | it('DateTimeLiteralNode (date, undefined)', function() { 130 | const node = FEEL.parse('date(a)', { startRule: 'SimpleExpressions' }); 131 | expect(node.type).to.equal('SimpleExpressions'); 132 | const dateTimeLiteralNode = node.simpleExpressions[0]; 133 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 134 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { }, builtInFns) }); 135 | expect(result).to.be.undefined; 136 | result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: undefined }, builtInFns) }); 137 | expect(result).to.be.undefined; 138 | }); 139 | 140 | it('DateTimeLiteralNode (date, null)', function() { 141 | const node = FEEL.parse('date(a)', { startRule: 'SimpleExpressions' }); 142 | expect(node.type).to.equal('SimpleExpressions'); 143 | const dateTimeLiteralNode = node.simpleExpressions[0]; 144 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 145 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: null }, builtInFns) }); 146 | expect(result).to.be.undefined; 147 | }); 148 | 149 | it('DateTimeLiteralNode (date and time, undefined)', function() { 150 | const node = FEEL.parse('date and time(a)', { startRule: 'SimpleExpressions' }); 151 | expect(node.type).to.equal('SimpleExpressions'); 152 | const dateTimeLiteralNode = node.simpleExpressions[0]; 153 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 154 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { }, builtInFns) }); 155 | expect(result).to.be.undefined; 156 | result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: undefined }, builtInFns) }); 157 | expect(result).to.be.undefined; 158 | }); 159 | 160 | it('DateTimeLiteralNode (date and time, null)', function() { 161 | const node = FEEL.parse('date and time(a)', { startRule: 'SimpleExpressions' }); 162 | expect(node.type).to.equal('SimpleExpressions'); 163 | const dateTimeLiteralNode = node.simpleExpressions[0]; 164 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 165 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: null }, builtInFns) }); 166 | expect(result).to.be.undefined; 167 | }); 168 | 169 | it('DateTimeLiteralNode (time, undefined)', function() { 170 | const node = FEEL.parse('time(a)', { startRule: 'SimpleExpressions' }); 171 | expect(node.type).to.equal('SimpleExpressions'); 172 | const dateTimeLiteralNode = node.simpleExpressions[0]; 173 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 174 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { }, builtInFns) }); 175 | expect(result).to.be.undefined; 176 | result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: undefined }, builtInFns) }); 177 | expect(result).to.be.undefined; 178 | }); 179 | 180 | it('DateTimeLiteralNode (time, null)', function() { 181 | const node = FEEL.parse('time(a)', { startRule: 'SimpleExpressions' }); 182 | expect(node.type).to.equal('SimpleExpressions'); 183 | const dateTimeLiteralNode = node.simpleExpressions[0]; 184 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 185 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: null }, builtInFns) }); 186 | expect(result).to.be.undefined; 187 | }); 188 | 189 | it('DateTimeLiteralNode (duration, undefined)', function() { 190 | const node = FEEL.parse('duration(a)', { startRule: 'SimpleExpressions' }); 191 | expect(node.type).to.equal('SimpleExpressions'); 192 | const dateTimeLiteralNode = node.simpleExpressions[0]; 193 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 194 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { }, builtInFns) }); 195 | expect(result).to.be.undefined; 196 | result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: undefined }, builtInFns) }); 197 | expect(result).to.be.undefined; 198 | }); 199 | 200 | it('DateTimeLiteralNode (duration, null)', function() { 201 | const node = FEEL.parse('duration(a)', { startRule: 'SimpleExpressions' }); 202 | expect(node.type).to.equal('SimpleExpressions'); 203 | const dateTimeLiteralNode = node.simpleExpressions[0]; 204 | expect(dateTimeLiteralNode.type).to.equal('DateTimeLiteral'); 205 | let result = dateTimeLiteralNode.build({ context: Object.assign({}, { a: null }, builtInFns) }); 206 | expect(result).to.be.undefined; 207 | }); 208 | 209 | it('FunctionInvocationNode', function() { 210 | const text = 'plus(42, 1)'; 211 | const _context = { 212 | plus: (x, y) => x + y 213 | }; 214 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 215 | expect(node.type).to.equal('SimpleExpressions'); 216 | const functionInvocationNode = node.simpleExpressions[0]; 217 | expect(functionInvocationNode.type).to.equal('FunctionInvocation'); 218 | const result = functionInvocationNode.build({ context: {} }); 219 | expect(result).to.equal(undefined); 220 | }); 221 | 222 | it('FunctionInvocationNode (built-in defined)', function() { 223 | const node = FEEL.parse('defined(a)', { startRule: 'SimpleUnaryTests' }); 224 | let result = node.build({ context: {} }); 225 | expect(result(false)).to.equal(true); 226 | expect(result(true)).to.equal(false); 227 | result = node.build({ context: { a: undefined} }); 228 | expect(result(false)).to.equal(true); 229 | expect(result(true)).to.equal(false); 230 | result = node.build({ context: { a: null} }); 231 | expect(result(false)).to.equal(true); 232 | expect(result(true)).to.equal(false); 233 | result = node.build({ context: { a: 42} }); 234 | expect(result(false)).to.equal(true); 235 | expect(result(true)).to.equal(false); 236 | }); 237 | 238 | it('PositionalParametersNode.build', function() { 239 | const text = 'plus(a, b)'; 240 | const _context = { 241 | plus: (x, y) => { 242 | if (x === undefined || y === undefined) { 243 | return undefined; 244 | } 245 | return x + y; 246 | }, 247 | a: undefined, 248 | }; 249 | const node = FEEL.parse(text, { startRule: 'SimpleExpressions' }); 250 | expect(node.type).to.equal('SimpleExpressions'); 251 | const functionInvocationNode = node.simpleExpressions[0]; 252 | const result = functionInvocationNode.build({ context: _context }); 253 | expect(result).to.be.undefined; 254 | }); 255 | }); 256 | -------------------------------------------------------------------------------- /test/string-functions.build.spec.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | const chalk = require('chalk'); 6 | const chai = require('chai'); 7 | const expect = chai.expect; 8 | const FEEL = require('../dist/feel'); 9 | 10 | describe(chalk.blue('Date/time/duration expression test'), function() { 11 | 12 | it('should find prefixes in string', function() { 13 | const condition = 'starts with(text, prefix)'; 14 | const parsedGrammar = FEEL.parse(condition); 15 | let result = parsedGrammar.build({ text: 'foobar', prefix: 'foo' }); 16 | expect(result).to.be.true; 17 | result = parsedGrammar.build({ text: 'foobar', prefix: 'bar' }); 18 | expect(result).to.be.false; 19 | result = parsedGrammar.build({ text: 'other', prefix: 'foo' }); 20 | expect(result).to.be.false; 21 | result = parsedGrammar.build({ text: null, prefix: 'foo' }); 22 | expect(result).to.be.null; 23 | result = parsedGrammar.build({ prefix: 'foo'}); 24 | expect(result).to.be.undefined; 25 | result = parsedGrammar.build({ text: 'foobar', prefix: null }); 26 | expect(result).to.be.null; 27 | result = parsedGrammar.build({ text: 'foobar', prefix: undefined }); 28 | expect(result).to.be.undefined; 29 | }); 30 | 31 | it('should find suffixes in string', function() { 32 | const condition = 'ends with(text, prefix)'; 33 | const parsedGrammar = FEEL.parse(condition); 34 | let result = parsedGrammar.build({ text: 'foobar', prefix: 'bar' }); 35 | expect(result).to.be.true; 36 | result = parsedGrammar.build({ text: 'foobar', prefix: 'foo' }); 37 | expect(result).to.be.false; 38 | result = parsedGrammar.build({ text: 'other', prefix: 'bar' }); 39 | expect(result).to.be.false; 40 | result = parsedGrammar.build({ text: null, prefix: 'bar' }); 41 | expect(result).to.be.null; 42 | result = parsedGrammar.build({ prefix: 'bar'}); 43 | expect(result).to.be.undefined; 44 | result = parsedGrammar.build({ text: 'foobar', prefix: null }); 45 | expect(result).to.be.null; 46 | result = parsedGrammar.build({ text: 'foobar', prefix: undefined }); 47 | expect(result).to.be.undefined; 48 | }); 49 | 50 | it('should find infixes in string', function() { 51 | const condition = 'contains(text, prefix)'; 52 | const parsedGrammar = FEEL.parse(condition); 53 | let result = parsedGrammar.build({ text: 'foobar', prefix: 'foo' }); 54 | expect(result).to.be.true; 55 | result = parsedGrammar.build({ text: 'foobar', prefix: 'bar' }); 56 | expect(result).to.be.true; 57 | result = parsedGrammar.build({ text: 'foobar', prefix: 'oob' }); 58 | expect(result).to.be.true; 59 | result = parsedGrammar.build({ text: 'foobar', prefix: 'no' }); 60 | expect(result).to.be.false; 61 | result = parsedGrammar.build({ text: 'other', prefix: 'bar' }); 62 | expect(result).to.be.false; 63 | result = parsedGrammar.build({ text: null, prefix: 'bar' }); 64 | expect(result).to.be.null; 65 | result = parsedGrammar.build({ prefix: 'bar'}); 66 | expect(result).to.be.undefined; 67 | result = parsedGrammar.build({ text: 'foobar', prefix: null }); 68 | expect(result).to.be.null; 69 | result = parsedGrammar.build({ text: 'foobar', prefix: undefined }); 70 | expect(result).to.be.undefined; 71 | }); 72 | 73 | it('should convert string to upper case', function() { 74 | const condition = 'upper case(text)'; 75 | const parsedGrammar = FEEL.parse(condition); 76 | let result = parsedGrammar.build({ text: 'foobar' }); 77 | expect(result).to.equal('FOOBAR'); 78 | result = parsedGrammar.build({ text: 'FOOBAR' }); 79 | expect(result).to.equal('FOOBAR'); 80 | result = parsedGrammar.build({ text: '' }); 81 | expect(result).to.equal(''); 82 | result = parsedGrammar.build({ text: null }); 83 | expect(result).to.equal(null); 84 | result = parsedGrammar.build({ }); 85 | expect(result).to.be.undefined; 86 | }); 87 | 88 | it('should convert string to lower case', function() { 89 | const condition = 'lower case(text)'; 90 | const parsedGrammar = FEEL.parse(condition); 91 | let result = parsedGrammar.build({ text: 'FOOBAR' }); 92 | expect(result).to.equal('foobar'); 93 | result = parsedGrammar.build({ text: 'foobar' }); 94 | expect(result).to.equal('foobar'); 95 | result = parsedGrammar.build({ text: '' }); 96 | expect(result).to.equal(''); 97 | result = parsedGrammar.build({ text: null }); 98 | expect(result).to.equal(null); 99 | result = parsedGrammar.build({ }); 100 | expect(result).to.be.undefined; 101 | }); 102 | 103 | }); 104 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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) => { 15 | let value = fn(y); 16 | if (value !== undefined) { 17 | value = !value; 18 | } 19 | return value; 20 | }; 21 | 22 | module.exports = { not }; 23 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | let result; 55 | if (args.length === 1) { 56 | const arg = args[0]; 57 | if (arg !== null && arg !== undefined) { 58 | let str; 59 | if (arg instanceof Date) { 60 | str = arg.toISOString(); 61 | } else if (arg.isDateTime || moment.isMoment(arg)) { 62 | str = arg.toISOString(); 63 | } else if (typeof arg === 'string') { 64 | str = arg; 65 | } else { 66 | throw new Error(`Invalid argument for date_and_time function: ${arg}`); 67 | } 68 | try { 69 | dt = str === '' ? moment() : parseIANATz(str) || moment.parseZone(str); 70 | } catch (err) { 71 | throw err; 72 | } 73 | if (!dt || !dt.isValid()) { 74 | throw new Error('Invalid date_and_time. This is usually caused by an invalid format. Please check the input format'); 75 | } 76 | } 77 | } else if (args.length === 2) { 78 | const [date, time] = args; 79 | if (date && date.isDate && time && time.isTime) { 80 | const datePart = date.format(date_ISO_8601); 81 | const timePart = time.format(time_ISO_8601); 82 | dt = moment.parseZone(`${datePart}${timePart}`); 83 | if (!dt.isValid()) { 84 | throw new Error('Invalid date and time. This is usually caused by input type mismatch.'); 85 | } 86 | } else { 87 | 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'); 88 | } 89 | } else { 90 | throw new Error('Invalid number of arguments specified with "date_and_time" in-built function'); 91 | } 92 | 93 | if (dt !== undefined) { 94 | result = addProperties(dt, props); 95 | } 96 | return result; 97 | }; 98 | 99 | module.exports = { 'date and time': dateAndTime }; 100 | -------------------------------------------------------------------------------- /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 | let result; 55 | if (args.length === 1) { 56 | const arg = args[0]; 57 | if (arg !== null && arg !== undefined) { 58 | if (typeof arg === 'string') { 59 | try { 60 | d = arg === '' ? moment.parseZone(UTCTimePart, time_ISO_8601) : parseDate(arg); 61 | } catch (err) { 62 | throw err; 63 | } 64 | } else if (typeof arg === 'object') { 65 | if (arg instanceof Date) { 66 | const ISO = arg.toISOString(); 67 | const dateTime = moment.parseZone(ISO); 68 | const datePart = dateTime.format(date_ISO_8601); 69 | d = moment.parseZone(`${datePart}${UTCTimePart}`); 70 | } else if (arg.isDateTime || moment.isMoment(arg)) { 71 | const dateTime = moment.tz(arg.format(), UTC); 72 | const datePart = dateTime.format(date_ISO_8601); 73 | d = moment.parseZone(`${datePart}${UTCTimePart}`); 74 | } 75 | if (!d || !d.isValid()) { 76 | throw new Error(`Invalid date. Parsing error while attempting to create date from ${arg}`); 77 | } 78 | } else { 79 | throw new Error('Invalid format encountered. Please specify date in one of these formats :\n- "date("2012-12-25")"\n- date_and_time object'); 80 | } 81 | } 82 | } else if (args.length === 3 && isNumber(args)) { 83 | const [year, month, day] = args; 84 | d = moment.tz({ year, month, day, hour: 0, minute: 0, second: 0 }, UTC); 85 | if (!d.isValid()) { 86 | throw new Error('Invalid date. Parsing error while attempting to create date from parts'); 87 | } 88 | } else { 89 | throw new Error('Invalid number of arguments specified with "date" in-built function'); 90 | } 91 | 92 | if (d !== undefined) { 93 | result = addProperties(d, props); 94 | } 95 | return result; 96 | }; 97 | 98 | module.exports = { date }; 99 | 100 | -------------------------------------------------------------------------------- /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 | let result; 80 | if (arg !== null && arg !== undefined) { 81 | if (typeof arg === 'string') { 82 | if (patternMatch(arg, ymd_ISO_8601)) { 83 | try { 84 | result = yearsAndMonthsDuration(arg); 85 | } catch (err) { 86 | throw err; 87 | } 88 | } else if (patternMatch(arg, dtd_ISO_8601)) { 89 | try { 90 | result = daysAndTimeDuration(arg); 91 | } catch (err) { 92 | throw err; 93 | } 94 | } 95 | if (result === undefined) { 96 | throw new Error('Invalid Format : "duration" built-in function. Please check the input format'); 97 | } 98 | } else { 99 | throw new Error(`Type Error : "duration" built-in function expects a string but "${typeof arg}" encountered`); 100 | } 101 | } 102 | return result; 103 | }; 104 | 105 | 106 | module.exports = { duration, 'years and months duration': yearsAndMonthsDuration, 'days and time duration': daysAndTimeDuration }; 107 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | let result; 98 | if (args.length === 1) { 99 | const arg = args[0]; 100 | if (arg !== null && arg !== undefined) { 101 | if (typeof arg === 'string') { 102 | try { 103 | t = arg === '' ? moment() : parseIANATz(arg) || parseTime(arg); 104 | } catch (err) { 105 | throw err; 106 | } 107 | } else if (typeof arg === 'object') { 108 | if (arg instanceof Date) { 109 | t = moment.parseZone(arg.toISOString); 110 | } else if (arg.isDateTime) { 111 | const str = arg.format(time_ISO_8601); 112 | t = moment.parseZone(str, time_ISO_8601); 113 | } 114 | if (!t.isValid()) { 115 | throw new Error('Invalid time. Parsing error while attempting to extract time from date and time.'); 116 | } 117 | } else { 118 | 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'); 119 | } 120 | } 121 | } else if (args.length >= 3 && isNumber(args.slice(0, 3))) { 122 | const [hour, minute, second] = args.slice(0, 3); 123 | t = moment({ hour, minute, second }); 124 | const dtd = args[3]; 125 | if (dtd) { 126 | try { 127 | const sign = Math.sign(dtd) < 0 ? '-' : '+'; 128 | const offset = `${sign}${dtdToOffset(dtd)}`; 129 | t = moment.parseZone(`${moment({ hour, minute, second }).format('THH:mm:ss')}${offset}`, time_ISO_8601); 130 | } catch (err) { 131 | throw new Error(`${err.message} - the fourth argument in "time" in-built function is expected to be of type "days and time duration"`); 132 | } 133 | } 134 | if (!t.isValid()) { 135 | throw new Error('Invalid time. Parsing error while attempting to create time from parts'); 136 | } 137 | } else { 138 | throw new Error('Invalid number of arguments specified with "time" in-built function'); 139 | } 140 | 141 | if (t !== undefined) { 142 | result = addProperties(t, props); 143 | } 144 | return result; 145 | }; 146 | 147 | module.exports = { time }; 148 | -------------------------------------------------------------------------------- /utils/built-in-functions/defined.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | const defined = y => y !== null && y !== undefined; 7 | 8 | module.exports = { defined }; 9 | -------------------------------------------------------------------------------- /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 defined = require('./defined'); 12 | const string = require('./string-functions'); 13 | 14 | module.exports = Object.assign({}, dateTime, list, boolean, defined, string); 15 | -------------------------------------------------------------------------------- /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 (list === undefined || list === null) { 12 | return list; 13 | } 14 | if (element === undefined || element === null) { 15 | return false; 16 | } 17 | if (!Array.isArray(list)) { 18 | throw new Error('operation unsupported on element of this type'); 19 | } else { 20 | return list.indexOf(element) > -1; 21 | } 22 | }; 23 | 24 | const count = (list) => { 25 | if (list === undefined || list === null) { 26 | return list; 27 | } 28 | if (!Array.isArray(list)) { 29 | throw new Error('operation unsupported on element of this type'); 30 | } else { 31 | return list.length; 32 | } 33 | }; 34 | 35 | const min = (list) => { 36 | if (list === undefined || list === null) { 37 | return list; 38 | } 39 | if (!Array.isArray(list)) { 40 | throw new Error('operation unsupported on element of this type'); 41 | } else { 42 | return _.min(list); 43 | } 44 | }; 45 | 46 | const max = (list) => { 47 | if (list === undefined || list === null) { 48 | return list; 49 | } 50 | if (!Array.isArray(list)) { 51 | throw new Error('operation unsupported on element of this type'); 52 | } else { 53 | return _.max(list); 54 | } 55 | }; 56 | 57 | const sum = (list) => { 58 | if (list === undefined || list === null) { 59 | return list; 60 | } 61 | if (!Array.isArray(list)) { 62 | throw new Error('operation unsupported on element of this type'); 63 | } else { 64 | return _.sum(list); 65 | } 66 | }; 67 | 68 | const mean = (list) => { 69 | if (list === undefined || list === null) { 70 | return list; 71 | } 72 | let result; 73 | if (!Array.isArray(list)) { 74 | throw new Error('operation unsupported on element of this type'); 75 | } else if (list.length > 0) { 76 | result = (_.sum(list)) / (list.length); 77 | } 78 | return result; 79 | }; 80 | 81 | const and = (list) => { 82 | if (list === undefined || list === null) { 83 | return list; 84 | } 85 | if (!Array.isArray(list)) { 86 | throw new Error('operation unsupported on element of this type'); 87 | } else { 88 | return list.reduce((recur, next) => recur && next, true); 89 | } 90 | }; 91 | 92 | const or = (list) => { 93 | if (list === undefined || list === null) { 94 | return list; 95 | } 96 | if (!Array.isArray(list)) { 97 | throw new Error('operation unsupported on element of this type'); 98 | } else { 99 | return list.reduce((recur, next) => recur || next, false); 100 | } 101 | }; 102 | 103 | const append = (list, element) => { 104 | if (list === undefined || list === null) { 105 | return list; 106 | } 107 | if (!Array.isArray(list)) { 108 | throw new Error('operation unsupported on element of this type'); 109 | } else if (element === undefined) { 110 | return list; 111 | } else if (Array.isArray(element)) { 112 | return list.concat(element); 113 | } else { 114 | return list.concat([element]); 115 | } 116 | }; 117 | 118 | const concatenate = (...args) => 119 | args.reduce((result, next) => { 120 | if (Array.isArray(next)) { 121 | return Array.prototype.concat(result, next); 122 | } 123 | return result; 124 | }, []); 125 | 126 | const insertBefore = (list, position, newItem) => { 127 | if (list === undefined || list === null) { 128 | return list; 129 | } 130 | if (position === undefined || position === null) { 131 | return position; 132 | } 133 | if (newItem === undefined) { 134 | return newItem; 135 | } 136 | if (!Array.isArray(list)) { 137 | throw new Error('operation unsupported on element of this type'); 138 | } else if (position > list.length || position < 0) { 139 | throw new Error(`cannot insert ${newItem} at position ${position} in list ${list}`); 140 | } else { 141 | const newList = [].concat(list); 142 | newList.splice(position, 0, newItem); 143 | return newList; 144 | } 145 | }; 146 | 147 | const remove = (list, position) => { 148 | if (list === undefined || list === null) { 149 | return list; 150 | } 151 | if (position === undefined || position === null) { 152 | return position; 153 | } 154 | if (!Array.isArray(list)) { 155 | throw new Error('operation unsupported on element of this type'); 156 | } else if (position > list.length - 1) { 157 | throw new Error(`cannot remove element at position ${position} in list ${list}`); 158 | } else { 159 | const newList = [].concat(list); 160 | newList.splice(position, 1); 161 | return newList; 162 | } 163 | }; 164 | 165 | const reverse = (list) => { 166 | if (list === undefined || list === null) { 167 | return list; 168 | } 169 | if (!Array.isArray(list)) { 170 | throw new Error('operation unsupported on element of this type'); 171 | } else { 172 | return _.reverse(list); 173 | } 174 | }; 175 | 176 | const indexOf = (list, match) => { 177 | if (list === undefined || list === null) { 178 | return list; 179 | } 180 | if (match === undefined) { 181 | return match; 182 | } 183 | if (!Array.isArray(list)) { 184 | throw new Error('operation unsupported on element of this type'); 185 | } else { 186 | const indexes = []; 187 | const remainingList = [].concat(list); 188 | let offset = 0; 189 | let nextIndex = remainingList.indexOf(match); 190 | while (nextIndex >= 0) { 191 | indexes.push(nextIndex + offset); 192 | remainingList.splice(0, nextIndex + 1); 193 | offset += nextIndex + 1; 194 | nextIndex = remainingList.indexOf(match); 195 | } 196 | return indexes; 197 | } 198 | }; 199 | 200 | const union = (...args) => _.union(...args); 201 | 202 | const distinctValues = (list) => { 203 | if (list === undefined || list === null) { 204 | return list; 205 | } 206 | if (!Array.isArray(list)) { 207 | throw new Error('operation unsupported on element of this type'); 208 | } else { 209 | return _.uniq(list); 210 | } 211 | }; 212 | 213 | const flatten = (...args) => { 214 | // remove context from args array (last element) 215 | const array = [].concat(args); 216 | array.splice(array.length - 1, 1); 217 | if (array.length === 1 && (array[0] === null || array[0] === undefined)) { 218 | return array[0]; 219 | } 220 | return _.flattenDeep(array); 221 | }; 222 | 223 | module.exports = { 224 | 'list contains': listContains, 225 | count, 226 | min, 227 | max, 228 | sum, 229 | mean, 230 | and, 231 | or, 232 | append, 233 | concatenate, 234 | 'insert before': insertBefore, 235 | remove, 236 | reverse, 237 | 'index of': indexOf, 238 | union, 239 | 'distinct values': distinctValues, 240 | flatten, 241 | }; 242 | -------------------------------------------------------------------------------- /utils/built-in-functions/string-functions/contains.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | /* 7 | Decision Model and Notation, v1.1 8 | Page : 133 9 | */ 10 | 11 | const contains = (text, prefix) => { 12 | let result; 13 | if (prefix === undefined || text === undefined) { 14 | result = undefined; 15 | } else if (prefix === null || text === null) { 16 | result = null; 17 | } else { 18 | result = text.indexOf(prefix) >= 0; 19 | } 20 | return result; 21 | }; 22 | 23 | module.exports = { contains }; 24 | -------------------------------------------------------------------------------- /utils/built-in-functions/string-functions/ends-with.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | /* 7 | Decision Model and Notation, v1.1 8 | Page : 133 9 | */ 10 | 11 | const endsWith = (text, prefix) => { 12 | let result; 13 | if (prefix === undefined || text === undefined) { 14 | result = undefined; 15 | } else if (prefix === null || text === null) { 16 | result = null; 17 | } else { 18 | result = text.lastIndexOf(prefix) === text.length - prefix.length; 19 | } 20 | return result; 21 | }; 22 | 23 | module.exports = { 'ends with': endsWith }; 24 | -------------------------------------------------------------------------------- /utils/built-in-functions/string-functions/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | module.exports = Object.assign({}, 7 | require('./contains'), 8 | require('./ends-with'), 9 | require('./lower-case'), 10 | require('./starts-with'), 11 | require('./upper-case')); 12 | -------------------------------------------------------------------------------- /utils/built-in-functions/string-functions/lower-case.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | /* 7 | Decision Model and Notation, v1.1 8 | Page : 133 9 | */ 10 | 11 | const lowerCase = (text) => { 12 | let result; 13 | if (text === undefined) { 14 | result = undefined; 15 | } else if (text === null) { 16 | result = null; 17 | } else { 18 | result = text.toLowerCase(); 19 | } 20 | return result; 21 | }; 22 | 23 | module.exports = { 'lower case': lowerCase }; 24 | -------------------------------------------------------------------------------- /utils/built-in-functions/string-functions/starts-with.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | /* 7 | Decision Model and Notation, v1.1 8 | Page : 133 9 | */ 10 | 11 | const startsWith = (text, prefix) => { 12 | let result; 13 | if (prefix === undefined || text === undefined) { 14 | result = undefined; 15 | } else if (prefix === null || text === null) { 16 | result = null; 17 | } else { 18 | result = text.indexOf(prefix) === 0; 19 | } 20 | return result; 21 | }; 22 | 23 | module.exports = { 'starts with': startsWith }; 24 | -------------------------------------------------------------------------------- /utils/built-in-functions/string-functions/upper-case.js: -------------------------------------------------------------------------------- 1 | /* 2 | * ©2017-2018 HBT Hamburger Berater Team GmbH 3 | * All Rights Reserved. 4 | */ 5 | 6 | /* 7 | Decision Model and Notation, v1.1 8 | Page : 133 9 | */ 10 | 11 | const upperCase = (text) => { 12 | let result; 13 | if (text === undefined) { 14 | result = undefined; 15 | } else if (text === null) { 16 | result = null; 17 | } else { 18 | result = text.toUpperCase(); 19 | } 20 | return result; 21 | }; 22 | 23 | module.exports = { 'upper case': upperCase }; 24 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 getOrderedOutput = (root, outputList) => { 182 | const policy = root.hitPolicy.charAt(0); 183 | let outputOrderedList = []; 184 | switch (policy) { 185 | case 'P': 186 | outputOrderedList.push(prepareOutputOrder(outputList, root.priorityList)[0]); 187 | break; 188 | case 'O': 189 | outputOrderedList = prepareOutputOrder(outputList, root.priorityList); 190 | break; 191 | case 'F': 192 | outputOrderedList = outputList.sort().slice(0, 1); 193 | break; 194 | case 'R': 195 | outputOrderedList = outputList.sort(); 196 | break; 197 | default : 198 | outputOrderedList = outputList; 199 | } 200 | return outputOrderedList; 201 | }; 202 | 203 | module.exports = { hitPolicyPass, getOrderedOutput }; 204 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 resolveName = (name, args) => args.context && args.context[name]; 9 | 10 | module.exports = resolveName; 11 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------