From 6bab04f207024172a73ef86a3c1673b5a4d2b867 Mon Sep 17 00:00:00 2001 From: Edson Tirelli Date: Sun, 7 May 2017 09:15:25 -0400 Subject: [PATCH] Drools 1535 - improving error reporting for unknown variables (#1237) * DROOLS-1535: adding compilation validation to DT input expressions * DROOLS-1535: Improving compilation to report unknown variables for decision table input expressions * DROOLS-1535: Improving compilation to report unknown variables for decision table input expressions * Drools 1535+tarilabs (#1) * Compile DT output to provide the users with compile time feedback on errors. --- .../runtime/events/FEELEventListener.java | 1 + .../core/compiler/DMNEvaluatorCompiler.java | 27 +- .../kie/dmn/core/compiler/DMNFEELHelper.java | 3 +- .../main/java/org/kie/dmn/core/util/Msg.java | 2 +- .../java/org/kie/dmn/core/DMNRuntimeTest.java | 57 +- .../dmn/core/SingleDecisionWithContext.dmn | 31 + .../org/kie/dmn/core/unknown_variable1.dmn | 1146 ++++++++++++++++ .../org/kie/dmn/core/unknown_variable2.dmn | 1152 +++++++++++++++++ .../kie/dmn/feel/parser/feel11/FEEL_1_1.g4 | 4 +- .../java/org/kie/dmn/feel/lang/Scope.java | 5 + .../kie/dmn/feel/lang/types/ScopeImpl.java | 13 + .../dmn/feel/parser/feel11/ParserHelper.java | 58 +- .../decisiontables/DTDecisionRule.java | 9 +- .../runtime/decisiontables/DTInputClause.java | 9 +- .../decisiontables/DecisionTableImpl.java | 10 +- .../functions/DecisionTableFunction.java | 37 +- .../main/java/org/kie/dmn/feel/util/Msg.java | 1 + .../lang/examples/CompileEvaluateTest.java | 57 +- .../org/kie/dmn/validation/ValidatorTest.java | 4 +- 19 files changed, 2580 insertions(+), 46 deletions(-) create mode 100644 kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/SingleDecisionWithContext.dmn create mode 100644 kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable1.dmn create mode 100644 kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable2.dmn diff --git a/kie-dmn/kie-dmn-api/src/main/java/org/kie/dmn/api/feel/runtime/events/FEELEventListener.java b/kie-dmn/kie-dmn-api/src/main/java/org/kie/dmn/api/feel/runtime/events/FEELEventListener.java index 905f0bab81a..d11831ff115 100644 --- a/kie-dmn/kie-dmn-api/src/main/java/org/kie/dmn/api/feel/runtime/events/FEELEventListener.java +++ b/kie-dmn/kie-dmn-api/src/main/java/org/kie/dmn/api/feel/runtime/events/FEELEventListener.java @@ -19,6 +19,7 @@ /** * A general interface for a FEEL event listener */ +@FunctionalInterface public interface FEELEventListener { void onEvent( FEELEvent event ); diff --git a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNEvaluatorCompiler.java b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNEvaluatorCompiler.java index eb2be73939d..1ed260c289b 100644 --- a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNEvaluatorCompiler.java +++ b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNEvaluatorCompiler.java @@ -239,6 +239,7 @@ private DMNExpressionEvaluator compileDecisionTable(DMNCompilerContext ctx, DMNM java.util.List inputs = new ArrayList<>(); int index = 0; for ( InputClause ic : dt.getInput() ) { + index++; String inputExpressionText = ic.getInputExpression().getText(); String inputValuesText = Optional.ofNullable( ic.getInputValues() ).map( UnaryTests::getText).orElse( null); java.util.List inputValues = null; @@ -250,13 +251,22 @@ private DMNExpressionEvaluator compileDecisionTable(DMNCompilerContext ctx, DMNM Msg.ERR_COMPILING_FEEL_EXPR_ON_DT_INPUT_CLAUSE_IDX, inputValuesText, node.getIdentifierString(), - ++index ); + index ); } else if ( ic.getInputExpression().getTypeRef() != null ) { QName inputExpressionTypeRef = ic.getInputExpression().getTypeRef(); BaseDMNTypeImpl typeRef = (BaseDMNTypeImpl) model.getTypeRegistry().resolveType(resolveNamespaceForTypeRef(inputExpressionTypeRef, ic.getInputExpression()), inputExpressionTypeRef.getLocalPart()); inputValues = typeRef.getAllowedValues(); } - inputs.add( new DTInputClause(inputExpressionText, inputValuesText, inputValues) ); + CompiledExpression compiledInput = feel.compileFeelExpression( + ctx, + inputExpressionText, + model, + dt, + Msg.ERR_COMPILING_FEEL_EXPR_ON_DT_INPUT_CLAUSE_IDX, + inputExpressionText, + dtName, + index ); + inputs.add( new DTInputClause(inputExpressionText, inputValuesText, inputValues, compiledInput ) ); } java.util.List outputs = new ArrayList<>(); index = 0; @@ -334,8 +344,17 @@ private DMNExpressionEvaluator compileDecisionTable(DMNCompilerContext ctx, DMNM } ) ); } for ( LiteralExpression le : dr.getOutputEntry() ) { - // we might want to compile and save the compiled expression here - rule.getOutputEntry().add( le.getText() ); + String expressionText = le.getText(); + CompiledExpression compiledExpression = feel.compileFeelExpression( + ctx, + expressionText, + model, + dr, + Msg.ERR_COMPILING_FEEL_EXPR_ON_DT_RULE_IDX, + expressionText, + dtName, + index+1 ); + rule.getOutputEntry().add( compiledExpression ); } rules.add( rule ); index++; diff --git a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNFEELHelper.java b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNFEELHelper.java index 5cfc4906b0e..0346077c708 100644 --- a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNFEELHelper.java +++ b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/compiler/DMNFEELHelper.java @@ -94,8 +94,7 @@ public List evaluateUnaryTests(DMNCompilerContext ctx, String unaryTe try { Map variableTypes = new HashMap<>(); for ( Map.Entry entry : ctx.getVariables().entrySet() ) { - // TODO: need to properly resolve types here - variableTypes.put( entry.getKey(), BuiltInType.UNKNOWN ); + variableTypes.put( entry.getKey(), ((BaseDMNTypeImpl) entry.getValue()).getFeelType() ); } result = feel.evaluateUnaryTests( unaryTests, variableTypes ); } catch( Throwable t ) { diff --git a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/util/Msg.java b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/util/Msg.java index ed5a3491b60..91748e5955b 100644 --- a/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/util/Msg.java +++ b/kie-dmn/kie-dmn-core/src/main/java/org/kie/dmn/core/util/Msg.java @@ -58,7 +58,7 @@ public class Msg { public static final Message3 ERROR_EVAL_NODE_DEP_WRONG_TYPE = new Message3( DMNMessageType.ERROR_EVAL_NODE, "Error while evaluating node '%s' for dependency '%s': the dependency value '%s' is not allowed by the declared type" ); public static final Message3 ERROR_EVAL_NODE_RESULT_WRONG_TYPE = new Message3( DMNMessageType.ERROR_EVAL_NODE, "Error while evaluating node '%s': the declared result type is '%s' but the actual value '%s' is not an instance of that type" ); public static final Message2 EXPR_TYPE_NOT_SUPPORTED_IN_NODE = new Message2( DMNMessageType.EXPR_TYPE_NOT_SUPPORTED_IN_NODE, "Expression type '%s' not supported in node '%s'" ); - public static final Message3 ERR_COMPILING_FEEL_EXPR_ON_DT_INPUT_CLAUSE_IDX = new Message3( DMNMessageType.ERR_COMPILING_FEEL, "Error compiling FEEL expression '%s' on decision table '%s', input clause #%s" ); + public static final Message4 ERR_COMPILING_FEEL_EXPR_ON_DT_INPUT_CLAUSE_IDX = new Message4( DMNMessageType.ERR_COMPILING_FEEL, "Error compiling FEEL expression '%s' on decision table '%s', input clause #%s: %s" ); public static final Message3 ERR_COMPILING_FEEL_EXPR_ON_DT_OUTPUT_CLAUSE_IDX = new Message3( DMNMessageType.ERR_COMPILING_FEEL, "Error compiling FEEL expression '%s' on decision table '%s', output clause #%s" ); public static final Message3 ERR_COMPILING_FEEL_EXPR_ON_DT_RULE_IDX = new Message3( DMNMessageType.ERR_COMPILING_FEEL, "Error compiling FEEL expression '%s' on decision table '%s', rule #%s" ); public static final Message2 ERR_COMPILING_FEEL_EXPR_ON_DT = new Message2( DMNMessageType.ERR_COMPILING_FEEL, "Error compiling FEEL expression '%s' on decision table '%s'" ); diff --git a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNRuntimeTest.java b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNRuntimeTest.java index 4e9482180d9..6084f450a7f 100644 --- a/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNRuntimeTest.java +++ b/kie-dmn/kie-dmn-core/src/test/java/org/kie/dmn/core/DMNRuntimeTest.java @@ -46,6 +46,7 @@ import static org.hamcrest.Matchers.hasEntry; import static org.hamcrest.Matchers.hasSize; import static org.junit.Assert.assertThat; +import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; @@ -434,20 +435,10 @@ public void testRelation() { @Test public void testMissingInputData() { DMNRuntime runtime = DMNRuntimeUtil.createRuntime( "missing_input_data.dmn", getClass() ); -// runtime.addListener( DMNRuntimeUtil.createListener() ); - DMNModel dmnModel = runtime.getModel( "http://www.trisotech.com/definitions/_4047acf3-fce2-42f3-abf2-fb06282c1ea0", "Upgrade Based On Promotions" ); assertThat( dmnModel, notNullValue() ); - assertThat( formatMessages( dmnModel.getMessages() ), dmnModel.hasErrors(), is( false ) ); - - DMNContext context = DMNFactory.newContext(); - context.set( "Requested Vehicle Class", "Compact" ); - context.set( "Membership Level", "Silver" ); - context.set( "Calendar Promotion", "None" ); - DMNResult dmnResult = runtime.evaluateAll( dmnModel, context ); - // System.out.println(formatMessages( dmnResult.getMessages() )); - // work in progress... later we will check the actual messages... - // assertThat( formatMessages( dmnResult.getMessages() ), dmnResult.getMessages( DMNMessage.Severity.ERROR ).size(), is( 4 ) ); + assertThat( formatMessages( dmnModel.getMessages() ), dmnModel.hasErrors(), is( true ) ); + assertThat( dmnModel.getMessages().get( 0 ).getMessageType(), is( DMNMessageType.ERR_COMPILING_FEEL ) ); } @Test @@ -1101,6 +1092,48 @@ public void testNPE() { assertThat( dmnResult.getMessages().stream().anyMatch( m -> m.getMessageType().equals( DMNMessageType.ERROR_EVAL_NODE ) ), is( true ) ); } + @Test + public void testUnknownVariable1() { + DMNRuntime runtime = DMNRuntimeUtil.createRuntime( "unknown_variable1.dmn", this.getClass() ); + DMNModel dmnModel = runtime.getModel( + "http://www.trisotech.com/definitions/_9105d4a6-6049-4ace-a9cd-88f18d29bc8f", + "Loan Recommendation - context" ); + assertThat( dmnModel, notNullValue() ); + assertThat( formatMessages( dmnModel.getMessages() ), dmnModel.getMessages().size(), is( 2 ) ); + assertEquals(1, dmnModel.getMessages().stream().filter( m -> m.getMessageType().equals(DMNMessageType.ERR_COMPILING_FEEL) ) + .filter( m -> m.getMessage().contains("Unknown variable 'NonSalaryPct'") ) + .count()); + } + + @Test + public void testUnknownVariable2() { + DMNRuntime runtime = DMNRuntimeUtil.createRuntime( "unknown_variable2.dmn", this.getClass() ); + DMNModel dmnModel = runtime.getModel( + "http://www.trisotech.com/definitions/_9105d4a6-6049-4ace-a9cd-88f18d29bc8f", + "Loan Recommendation - context" ); + assertThat( dmnModel, notNullValue() ); + assertThat( formatMessages( dmnModel.getMessages() ), dmnModel.getMessages().size(), is( 1 ) ); + assertThat( dmnModel.getMessages().get( 0 ).getMessageType(), is( DMNMessageType.ERR_COMPILING_FEEL ) ); + assertThat( dmnModel.getMessages().get( 0 ).getMessage(), containsString( "Unknown variable 'Borrower.liquidAssetsAmt'" ) ); + } + + @Test + public void testSingleDecisionWithContext() { + DMNRuntime runtime = DMNRuntimeUtil.createRuntime( "SingleDecisionWithContext.dmn", this.getClass() ); + DMNModel dmnModel = runtime.getModel( + "http://www.trisotech.com/definitions/_71af58db-e1df-4b0f-aee2-48e0e8d89672", + "SingleDecisionWithContext" ); + assertThat( dmnModel, notNullValue() ); + assertThat( formatMessages( dmnModel.getMessages() ), dmnModel.hasErrors(), is( false ) ); + + DMNContext emptyContext = runtime.newContext(); + DMNResult dmnResult = runtime.evaluateAll( dmnModel, emptyContext ); + + assertThat( formatMessages( dmnResult.getMessages() ), dmnResult.hasErrors(), is( false ) ); + DMNContext result = dmnResult.getContext(); + assertThat( result.get( "MyDecision" ), is( "Hello John Doe" ) ); + } + private String formatMessages(List messages) { return messages.stream().map( m -> m.toString() ).collect( Collectors.joining( "\n" ) ); } diff --git a/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/SingleDecisionWithContext.dmn b/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/SingleDecisionWithContext.dmn new file mode 100644 index 00000000000..b33be1c68e8 --- /dev/null +++ b/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/SingleDecisionWithContext.dmn @@ -0,0 +1,31 @@ + + + + + + + + + + + + + "John" + + + + + + "Doe" + + + + + + + "Hello " + MyPerson.FirstName + " " + MyPerson.LastName + + + + + diff --git a/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable1.dmn b/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable1.dmn new file mode 100644 index 00000000000..30919739ad4 --- /dev/null +++ b/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable1.dmn @@ -0,0 +1,1146 @@ + + + + + feel:string + + "Approve","Decline" + + + + feel:string + + "A","B","C","D","X" + + + + feel:string + + "High","Medium","Low" + + + + feel:string + + "Not affordable","Marginal","Affordable" + + + + + feel:number + + + feel:number + + + feel:number + + + + feel:number + + >=18 + + + + feel:string + + "Employed","Self-employed","Retired","Unemployed" + + + + + tAge + + + tEmploymentStatus + + + feel:number + + + feel:number + + + feel:number + + + feel:number + + + feel:number + + + + feel:string + + "High","Medium","Low" + + + + feel:number + + [300..850] + + + + + + + + + + + + + + + + + Collateral Risk Category + + + "High","Medium","Low" + + + + + Affordability Category + + + "Not affordable","Marginal","Affordable" + + + + + Credit Risk Category + + + "A","B","C","D","X" + + + + + "Approve","Decline" + + + + + "Medium" + + + "Marginal" + + + "A" + + + "Approve" + + + + + "Medium" + + + "Affordable" + + + "A","B" + + + "Approve" + + + + + "Low" + + + "Marginal" + + + "A","B","C" + + + "Approve" + + + + + "Low" + + + "Affordable" + + + "A","B","C","D" + + + "Approve" + + + + + - + + + - + + + - + + + "Decline" + + + + + + + + + + + + + LTV + + + + + "High","Medium","Low" + + + + + >0.80 + + + "High" + + + + + (0.75..0.80] + + + "Medium" + + + + + <=0.75 + + + "Low" + + + + + + + + + + + + + + + + Income Risk Category + + + "High","Medium","Low" + + + + + DTI + + + + + "Not affordable","Marginal","Affordable" + + + + + "High" + + + - + + + "Not affordable" + + + + + - + + + >0.45 + + + "Not affordable" + + + + + "Medium" + + + (0.36..0.45] + + + "Not affordable" + + + + + "Low" + + + (0.36..0.45] + + + "Marginal" + + + + + "Medium" + + + <=0.36 + + + "Marginal" + + + + + "Low" + + + <=0.36 + + + "Affordable" + + + + + + + + + + + + + + + + + + + + + + + (Borrower.liquidAssetsAmt - Down Payment Amount)/Loan Payment + + + + + + + + Credit Score + + + [300..850] + + + + + Reserves Months + + + + + "A","B","C","D","X" + + + + + >700 + + + - + + + "A" + + + + + [680..700] + + + >=2 + + + "A" + + + + + [680..700] + + + <2 + + + "B" + + + + + [660..680) + + + >=6 + + + "B" + + + + + [660..680) + + + <6 + + + "C" + + + + + [620..660) + + + >=2 + + + "C" + + + + + [620..660) + + + <2 + + + "D" + + + + + <620 + + + - + + + "X" + + + + + + + category + + + + + + + + + + + + + + Loan Amount/Appraised Value + + + + + + + + + + + + + + + (Loan Payment + Borrower.MonthlyDebtPmtAmt)/(Borrower.TotalAnnualIncome/12) + + + + + + + + + + + + + + Borrower.Age + + + >=18 + + + + + Borrower.EmploymentStatus + + + "Employed","Self-employed","Retired","Unemployed" + + + + + Borrower.YearsAtCurrentEmployer + + + + + NonSalaryPct + + + + + + [18..35] + + + - + + + - + + + - + + + 30 + + + + + (35..55] + + + - + + + - + + + - + + + 50 + + + + + (55..65] + + + - + + + - + + + - + + + 30 + + + + + >65 + + + - + + + - + + + - + + + 0 + + + + + - + + + "Employed" + + + >3 + + + - + + + 75 + + + + + - + + + "Employed" + + + [1..3] + + + - + + + 40 + + + + + - + + + "Employed" + + + <1 + + + - + + + 10 + + + + + - + + + "Self-employed" + + + >3 + + + - + + + 50 + + + + + - + + + "Self-employed" + + + [1..3] + + + - + + + 20 + + + + + - + + + "Self-employed" + + + <1 + + + - + + + 10 + + + + + - + + + "Retired" + + + - + + + - + + + 10 + + + + + - + + + "Unemployed" + + + - + + + - + + + 0 + + + + + - + + + "Employed" + + + - + + + <20 + + + 20 + + + + + - + + + "Employed" + + + - + + + >40 + + + -30 + + + + + + + + + + Income Risk Score + + + + + "High","Medium","Low" + + + + + >70 + + + "Low" + + + + + [10..70] + + + "Medium" + + + + + <10 + + + "High" + + + + + + + category + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LTV + + + + + Credit Score + + + [300..850] + + + + + + <=0.60 + + + >=660 + + + 0 + + + + + <=0.60 + + + [620..660) + + + 0.5 + + + + + (0.60..0.70] + + + >=720 + + + 0.25 + + + + + (0.60..0.70] + + + [680..720) + + + 0.5 + + + + + (0.60..0.70] + + + [660..680) + + + 1.0 + + + + + (0.60..0.70] + + + [640..660) + + + 1.25 + + + + + (0.60..0.70] + + + [620..640) + + + 1.5 + + + + + (0.70..0.75] + + + >=740 + + + 0.25 + + + + + (0.70..0.75] + + + [720..740) + + + 0.5 + + + + + (0.70..0.75] + + + [700..720) + + + 1.0 + + + + + (0.70..0.75] + + + [680..700) + + + 1.25 + + + + + (0.70..0.75] + + + [660..680) + + + 2.25 + + + + + (0.70..0.75] + + + [640..660) + + + 2.75 + + + + + (0.70..0.75] + + + [620..640) + + + 3.0 + + + + + (0.75..0.80] + + + >=740 + + + 0.5 + + + + + (0.75..0.80] + + + [720..740) + + + 0.75 + + + + + (0.75..0.80] + + + [700..720) + + + 1.25 + + + + + (0.75..0.80] + + + [680..700) + + + 1.75 + + + + + (0.75..0.80] + + + [660..680) + + + 2.75 + + + + + (0.75..0.80] + + + [620..660) + + + 3.0 + + + + + - + + + <620 + + + null + + + + + >0.80 + + + - + + + null + + + + + + + + 0.00125*LLPA + Best rate + + + + + payment(Loan Amount, Adjusted Rate, 360) + + + + + + + + + + + + + + + + + + + + + + + principal*rate/12/(1-(1+rate/12)**-term) + + + + + diff --git a/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable2.dmn b/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable2.dmn new file mode 100644 index 00000000000..aed4c352597 --- /dev/null +++ b/kie-dmn/kie-dmn-core/src/test/resources/org/kie/dmn/core/unknown_variable2.dmn @@ -0,0 +1,1152 @@ + + + + + feel:string + + "Approve","Decline" + + + + feel:string + + "A","B","C","D","X" + + + + feel:string + + "High","Medium","Low" + + + + feel:string + + "Not affordable","Marginal","Affordable" + + + + + feel:number + + + feel:number + + + feel:number + + + + feel:number + + >=18 + + + + feel:string + + "Employed","Self-employed","Retired","Unemployed" + + + + + tAge + + + tEmploymentStatus + + + feel:number + + + feel:number + + + feel:number + + + feel:number + + + feel:number + + + + feel:string + + "High","Medium","Low" + + + + feel:number + + [300..850] + + + + + + + + + + + + + + + + + Collateral Risk Category + + + "High","Medium","Low" + + + + + Affordability Category + + + "Not affordable","Marginal","Affordable" + + + + + Credit Risk Category + + + "A","B","C","D","X" + + + + + "Approve","Decline" + + + + + "Medium" + + + "Marginal" + + + "A" + + + "Approve" + + + + + "Medium" + + + "Affordable" + + + "A","B" + + + "Approve" + + + + + "Low" + + + "Marginal" + + + "A","B","C" + + + "Approve" + + + + + "Low" + + + "Affordable" + + + "A","B","C","D" + + + "Approve" + + + + + - + + + - + + + - + + + "Decline" + + + + + + + + + + + + + LTV + + + + + "High","Medium","Low" + + + + + >0.80 + + + "High" + + + + + (0.75..0.80] + + + "Medium" + + + + + <=0.75 + + + "Low" + + + + + + + + + + + + + + + + Income Risk Category + + + "High","Medium","Low" + + + + + DTI + + + + + "Not affordable","Marginal","Affordable" + + + + + "High" + + + - + + + "Not affordable" + + + + + - + + + >0.45 + + + "Not affordable" + + + + + "Medium" + + + (0.36..0.45] + + + "Not affordable" + + + + + "Low" + + + (0.36..0.45] + + + "Marginal" + + + + + "Medium" + + + <=0.36 + + + "Marginal" + + + + + "Low" + + + <=0.36 + + + "Affordable" + + + + + + + + + + + + + + + + + + + + + + + (Borrower.liquidAssetsAmt - Down Payment Amount)/Loan Payment + + + + + + + + Credit Score + + + [300..850] + + + + + Reserves Months + + + + + "A","B","C","D","X" + + + + + >700 + + + - + + + "A" + + + + + [680..700] + + + >=2 + + + "A" + + + + + [680..700] + + + <2 + + + "B" + + + + + [660..680) + + + >=6 + + + "B" + + + + + [660..680) + + + <6 + + + "C" + + + + + [620..660) + + + >=2 + + + "C" + + + + + [620..660) + + + <2 + + + "D" + + + + + <620 + + + - + + + "X" + + + + + + + category + + + + + + + + + + + + + + Loan Amount/Appraised Value + + + + + + + + + + + + + + + (Loan Payment + Borrower.MonthlyDebtPmtAmt)/(Borrower.TotalAnnualIncome/12) + + + + + + + + + + + + Borrower.NonSalaryIncome/Borrower.TotalAnnualIncome *100 + + + + + + + + Borrower.Age + + + >=18 + + + + + Borrower.EmploymentStatus + + + "Employed","Self-employed","Retired","Unemployed" + + + + + Borrower.YearsAtCurrentEmployer + + + + + NonSalaryPct + + + + + + [18..35] + + + - + + + - + + + - + + + 30 + + + + + (35..55] + + + - + + + - + + + - + + + 50 + + + + + (55..65] + + + - + + + - + + + - + + + 30 + + + + + >65 + + + - + + + - + + + - + + + 0 + + + + + - + + + "Employed" + + + >3 + + + - + + + 75 + + + + + - + + + "Employed" + + + [1..3] + + + - + + + 40 + + + + + - + + + "Employed" + + + <1 + + + - + + + 10 + + + + + - + + + "Self-employed" + + + >3 + + + - + + + 50 + + + + + - + + + "Self-employed" + + + [1..3] + + + - + + + 20 + + + + + - + + + "Self-employed" + + + <1 + + + - + + + 10 + + + + + - + + + "Retired" + + + - + + + - + + + 10 + + + + + - + + + "Unemployed" + + + - + + + - + + + 0 + + + + + - + + + "Employed" + + + - + + + <20 + + + 20 + + + + + - + + + "Employed" + + + - + + + >40 + + + -30 + + + + + + + + + + Income Risk Score + + + + + "High","Medium","Low" + + + + + >70 + + + "Low" + + + + + [10..70] + + + "Medium" + + + + + <10 + + + "High" + + + + + + + category + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + LTV + + + + + Credit Score + + + [300..850] + + + + + + <=0.60 + + + >=660 + + + 0 + + + + + <=0.60 + + + [620..660) + + + 0.5 + + + + + (0.60..0.70] + + + >=720 + + + 0.25 + + + + + (0.60..0.70] + + + [680..720) + + + 0.5 + + + + + (0.60..0.70] + + + [660..680) + + + 1.0 + + + + + (0.60..0.70] + + + [640..660) + + + 1.25 + + + + + (0.60..0.70] + + + [620..640) + + + 1.5 + + + + + (0.70..0.75] + + + >=740 + + + 0.25 + + + + + (0.70..0.75] + + + [720..740) + + + 0.5 + + + + + (0.70..0.75] + + + [700..720) + + + 1.0 + + + + + (0.70..0.75] + + + [680..700) + + + 1.25 + + + + + (0.70..0.75] + + + [660..680) + + + 2.25 + + + + + (0.70..0.75] + + + [640..660) + + + 2.75 + + + + + (0.70..0.75] + + + [620..640) + + + 3.0 + + + + + (0.75..0.80] + + + >=740 + + + 0.5 + + + + + (0.75..0.80] + + + [720..740) + + + 0.75 + + + + + (0.75..0.80] + + + [700..720) + + + 1.25 + + + + + (0.75..0.80] + + + [680..700) + + + 1.75 + + + + + (0.75..0.80] + + + [660..680) + + + 2.75 + + + + + (0.75..0.80] + + + [620..660) + + + 3.0 + + + + + - + + + <620 + + + null + + + + + >0.80 + + + - + + + null + + + + + + + + 0.00125*LLPA + Best rate + + + + + payment(Loan Amount, Adjusted Rate, 360) + + + + + + + + + + + + + + + + + + + + + + + principal*rate/12/(1-(1+rate/12)**-term) + + + + + diff --git a/kie-dmn/kie-dmn-feel/src/main/antlr4/org/kie/dmn/feel/parser/feel11/FEEL_1_1.g4 b/kie-dmn/kie-dmn-feel/src/main/antlr4/org/kie/dmn/feel/parser/feel11/FEEL_1_1.g4 index 62b62af7dd5..428c1cfbc6d 100644 --- a/kie-dmn/kie-dmn-feel/src/main/antlr4/org/kie/dmn/feel/parser/feel11/FEEL_1_1.g4 +++ b/kie-dmn/kie-dmn-feel/src/main/antlr4/org/kie/dmn/feel/parser/feel11/FEEL_1_1.g4 @@ -133,7 +133,7 @@ functionDefinition @after { helper.popScope(); } - : function_key '(' formalParameters? ')' external=external_key? body=expression + : function_key '(' formalParameters? ')' external=external_key? {helper.enableDynamicResolution();} body=expression {helper.disableDynamicResolution();} ; formalParameters @@ -332,7 +332,7 @@ qualifiedName ( '.' {helper.recoverScope( name ); count++;} n2=nameRef - {name=getOriginalText( $n2.ctx );} + {name=getOriginalText( $n2.ctx ); qn.add( name ); helper.validateVariable( $n1.ctx, qn, name ); } )* ; diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/Scope.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/Scope.java index 7fa7d0d38d3..7126e57f087 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/Scope.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/Scope.java @@ -106,4 +106,9 @@ public interface Scope { Map getSymbols(); + /** + * maybe null. + */ + Type getType(); + } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/types/ScopeImpl.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/types/ScopeImpl.java index c5b6be60e4f..9a4d1b6f621 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/types/ScopeImpl.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/lang/types/ScopeImpl.java @@ -21,6 +21,7 @@ import org.kie.dmn.feel.parser.feel11.FEEL_1_1Lexer; import org.kie.dmn.feel.lang.Scope; import org.kie.dmn.feel.lang.Symbol; +import org.kie.dmn.feel.lang.Type; import org.kie.dmn.feel.util.EvalHelper; import org.kie.dmn.feel.util.TokenTree; import org.slf4j.Logger; @@ -41,6 +42,8 @@ public class ScopeImpl private TokenTree tokenTree; + private Type type; + public ScopeImpl() { } @@ -52,6 +55,11 @@ public ScopeImpl(String name, Scope parentScope) { } } + public ScopeImpl(String name, Scope parentScope, Type type) { + this(name, parentScope); + this.type = type; + } + public String getName() { return name; } @@ -167,4 +175,9 @@ public String toString() { ", parentScope='" + ( parentScope != null ? parentScope.getName() : "" ) + "' }"; } + + @Override + public Type getType() { + return this.type; + } } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/parser/feel11/ParserHelper.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/parser/feel11/ParserHelper.java index e1ed5b200b3..98da4706c10 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/parser/feel11/ParserHelper.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/parser/feel11/ParserHelper.java @@ -26,6 +26,7 @@ import org.kie.dmn.feel.lang.impl.FEELEventListenersManager; import org.kie.dmn.feel.lang.impl.JavaBackedType; import org.kie.dmn.feel.lang.Scope; +import org.kie.dmn.feel.lang.SimpleType; import org.kie.dmn.feel.lang.Symbol; import org.kie.dmn.feel.lang.Type; import org.kie.dmn.feel.lang.types.BuiltInType; @@ -79,6 +80,11 @@ public void pushScope() { LOG.trace("pushScope()"); currentScope = new ScopeImpl( currentName.peek(), currentScope ); } + + public void pushScope(Type type) { + LOG.trace("pushScope()"); + currentScope = new ScopeImpl( currentName.peek(), currentScope, type ); + } public void popScope() { LOG.trace("popScope()"); @@ -108,16 +114,63 @@ public void recoverScope( String name ) { Scope s = this.currentScope.getChildScopes().get( name ); if( s != null ) { currentScope = s; + if ( currentScope.getType() != null && currentScope.getType().equals(BuiltInType.UNKNOWN) ) { + enableDynamicResolution(); + } } else { Symbol resolved = this.currentScope.resolve(name); if ( resolved != null && resolved.getType() instanceof CompositeType ) { pushName(name); - pushScope(); + pushScope(resolved.getType()); CompositeType type = (CompositeType) resolved.getType(); for ( Map.Entry f : type.getFields().entrySet() ) { this.currentScope.define(new VariableSymbol( f.getKey(), f.getValue() )); } LOG.trace(".. PUSHED, scope name {} with symbols {}", this.currentName.peek(), this.currentScope.getSymbols()); + } else if ( resolved != null && resolved.getType() instanceof BuiltInType ) { + BuiltInType resolvedBIType = (BuiltInType) resolved.getType(); + pushName(name); + pushScope(resolvedBIType); + switch (resolvedBIType) { + // FEEL spec table 53 + case DATE: + this.currentScope.define(new VariableSymbol( "year", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "month", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "day", BuiltInType.NUMBER )); + break; + case TIME: + this.currentScope.define(new VariableSymbol( "hour", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "minute", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "second", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "time offset", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "timezone", BuiltInType.NUMBER )); + break; + case DATE_TIME: + this.currentScope.define(new VariableSymbol( "year", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "month", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "day", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "hour", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "minute", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "second", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "time offset", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "timezone", BuiltInType.NUMBER )); + break; + case DURATION: + // TODO might need to distinguish between `years and months duration` and `days and time duration` + this.currentScope.define(new VariableSymbol( "years", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "months", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "days", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "hours", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "minutes", BuiltInType.NUMBER )); + this.currentScope.define(new VariableSymbol( "seconds", BuiltInType.NUMBER )); + break; + // table 53 applies only to type(e) is a date/time/duration + case UNKNOWN: + enableDynamicResolution(); + break; + default: + break; + } } else { pushScope(); } @@ -126,6 +179,9 @@ public void recoverScope( String name ) { public void dismissScope() { LOG.trace("dismissScope()"); + if ( currentScope.getType() != null && currentScope.getType().equals(BuiltInType.UNKNOWN) ) { + disableDynamicResolution(); + } popScope(); } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTDecisionRule.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTDecisionRule.java index 9ba75c845c3..4949e48082a 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTDecisionRule.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTDecisionRule.java @@ -16,6 +16,7 @@ package org.kie.dmn.feel.runtime.decisiontables; +import org.kie.dmn.feel.lang.CompiledExpression; import org.kie.dmn.feel.runtime.UnaryTest; import org.kie.dmn.model.v1_1.LiteralExpression; @@ -49,9 +50,9 @@ The class DecisionRule is used to model the rules in a decision table (see 8.2 N th OutputClause. */ public class DTDecisionRule { - private int index; - private List inputEntry; - private List outputEntry; + private int index; + private List inputEntry; + private List outputEntry; public DTDecisionRule(int index) { this.index = index; @@ -74,7 +75,7 @@ public List getInputEntry() { the output components of this DecisionRule. * @return */ - public List getOutputEntry() { + public List getOutputEntry() { if ( outputEntry == null ) { outputEntry = new ArrayList<>(); } diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTInputClause.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTInputClause.java index 97a4090cdac..18caaa539a3 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTInputClause.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DTInputClause.java @@ -4,14 +4,16 @@ import java.util.Collections; import java.util.List; +import org.kie.dmn.feel.lang.CompiledExpression; import org.kie.dmn.feel.runtime.UnaryTest; public class DTInputClause { private final String inputExpression; + private final CompiledExpression compiledInput; private final String inputValuesText; private final List inputValues; - public DTInputClause(String inputExpression, String inputValuesText, List inputValues) { + public DTInputClause(String inputExpression, String inputValuesText, List inputValues, CompiledExpression compiledInput) { super(); this.inputExpression = inputExpression; this.inputValuesText = inputValuesText; @@ -20,6 +22,7 @@ public DTInputClause(String inputExpression, String inputValuesText, List getInputValues() { public String getInputValuesText() { return inputValuesText; } + + public CompiledExpression getCompiledInput() { + return compiledInput; + } } \ No newline at end of file diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java index 1b491bea9f3..d0b262cc669 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/decisiontables/DecisionTableImpl.java @@ -19,6 +19,7 @@ import org.kie.dmn.api.feel.runtime.events.FEELEvent; import org.kie.dmn.api.feel.runtime.events.FEELEvent.Severity; import org.kie.dmn.feel.FEEL; +import org.kie.dmn.feel.lang.CompiledExpression; import org.kie.dmn.feel.lang.EvaluationContext; import org.kie.dmn.feel.lang.impl.FEELEventListenersManager; import org.kie.dmn.feel.runtime.UnaryTest; @@ -113,7 +114,12 @@ private Object[] resolveActualInputs(EvaluationContext ctx, FEEL feel) { Map variables = ctx.getAllValues(); Object[] actualInputs = new Object[ inputs.size() ]; for( int i = 0; i < inputs.size(); i++ ) { - actualInputs[i] = feel.evaluate( inputs.get( i ).getInputExpression(), variables ); + CompiledExpression compiledInput = inputs.get( i ).getCompiledInput(); + if( compiledInput != null ) { + actualInputs[i] = feel.evaluate( compiledInput, variables ); + } else { + actualInputs[i] = feel.evaluate( inputs.get( i ).getInputExpression(), variables ); + } } return actualInputs; } @@ -209,7 +215,7 @@ private List evaluateResults(EvaluationContext ctx, FEEL feel, Object[] * Each hit results in one output value (multiple outputs are collected into a single context value) */ private Object hitToOutput(EvaluationContext ctx, FEEL feel, DTDecisionRule rule) { - List outputEntries = rule.getOutputEntry(); + List outputEntries = rule.getOutputEntry(); Map values = ctx.getAllValues(); if ( outputEntries.size() == 1 ) { Object value = feel.evaluate( outputEntries.get( 0 ), values ); diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DecisionTableFunction.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DecisionTableFunction.java index ccb345ee91e..b724784de4e 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DecisionTableFunction.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/runtime/functions/DecisionTableFunction.java @@ -16,12 +16,16 @@ package org.kie.dmn.feel.runtime.functions; +import org.kie.dmn.api.feel.runtime.events.FEELEvent; +import org.kie.dmn.api.feel.runtime.events.FEELEventListener; import org.kie.dmn.feel.FEEL; +import org.kie.dmn.feel.lang.CompiledExpression; +import org.kie.dmn.feel.lang.EvaluationContext; import org.kie.dmn.feel.runtime.Range; import org.kie.dmn.feel.runtime.UnaryTest; import org.kie.dmn.feel.runtime.decisiontables.*; -import org.kie.dmn.model.v1_1.DMNElement; -import org.kie.dmn.model.v1_1.DecisionTable; +import org.kie.dmn.feel.runtime.events.FEELEventBase; +import org.kie.dmn.feel.util.Msg; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -58,7 +62,7 @@ list element is a unary tests literal (see below). composed of outputs and output values; else default output value is one of the output values. */ - public Object invoke( + public Object invoke(@ParameterName("ctx") EvaluationContext ctx, @ParameterName("outputs") Object outputs, @ParameterName("input expression list") Object inputExpressionList, @ParameterName("input values list") List inputValuesList, @@ -78,10 +82,10 @@ public Object invoke( } // zip inputExpression with its inputValue inputs = IntStream.range( 0, inputExpressions.size() ) - .mapToObj( i -> new DTInputClause( inputExpressions.get( i ), inputValuesList.toString(), Collections.singletonList( inputValues.get( i ) ) ) ) + .mapToObj( i -> new DTInputClause( inputExpressions.get( i ), inputValuesList.toString(), Collections.singletonList( inputValues.get( i ) ), null ) ) .collect( Collectors.toList() ); } else { - inputs = inputExpressions.stream().map( ie -> new DTInputClause( ie, null, null ) ).collect( Collectors.toList() ); + inputs = inputExpressions.stream().map( ie -> new DTInputClause( ie, null, null, null ) ).collect( Collectors.toList() ); } List parseOutputs = outputs instanceof List ? (List) outputs : Collections.singletonList( (String) outputs ); @@ -103,8 +107,9 @@ public Object invoke( } // TODO parse default output value. + FEEL feel = FEEL.newInstance(); List decisionRules = IntStream.range( 0, ruleList.size() ) - .mapToObj( index -> DecisionTableFunction.toDecisionRule( index, ruleList.get( index ), inputExpressions.size() ) ) + .mapToObj( index -> toDecisionRule( ctx, feel, index, ruleList.get( index ), inputExpressions.size() ) ) .collect( Collectors.toList() ); // TODO is there a way to avoid UUID and get from _evaluation_ ctx the name of the wrapping context? @@ -127,7 +132,16 @@ protected List> objectToUnaryTestList(List> values) return tests; } - public static DTDecisionRule toDecisionRule(int index, List rule, int inputSize) { + /** + * Convert row to DTDecisionRule + * @param mainCtx the main context is used to identify the hosted FEELEventManager + * @param embeddedFEEL a possibly cached embedded FEEL to compile the output expression, error will be reported up to the mainCtx + * @param index + * @param rule + * @param inputSize + * @return + */ + private static DTDecisionRule toDecisionRule(EvaluationContext mainCtx, FEEL embeddedFEEL, int index, List rule, int inputSize) { // TODO should be check indeed block of inputSize n inputs, followed by block of outputs. DTDecisionRule dr = new DTDecisionRule( index ); for ( int i = 0; i < rule.size(); i++ ) { @@ -135,8 +149,13 @@ public static DTDecisionRule toDecisionRule(int index, List rule, int inputSi if ( i < inputSize ) { dr.getInputEntry().add( toUnaryTest( o ) ); } else { - // TODO: should we pre-compile the expression? probably... - dr.getOutputEntry().add( (String) o ); + FEELEventListener ruleListener = event -> mainCtx.notifyEvt( () -> new FEELEventBase(event.getSeverity(), + Msg.createMessage(Msg.ERROR_COMPILE_EXPR_DT_FUNCTION_RULE_IDX, index+1, event.getMessage()), + event.getSourceException())); + embeddedFEEL.addListener(ruleListener); + CompiledExpression compiledExpression = embeddedFEEL.compile((String) o, embeddedFEEL.newCompilerContext()); + dr.getOutputEntry().add( compiledExpression ); + embeddedFEEL.removeListener(ruleListener); } } return dr; diff --git a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/Msg.java b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/Msg.java index 6a315257914..148bc33e9ba 100644 --- a/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/Msg.java +++ b/kie-dmn/kie-dmn-feel/src/main/java/org/kie/dmn/feel/util/Msg.java @@ -44,6 +44,7 @@ public class Msg { public static final Message2 INVALID_VARIABLE_NAME = new Message2( "A variable name cannot contain the %s '%s'"); public static final Message2 INVALID_VARIABLE_NAME_START = new Message2( "A variable name cannot start with the %s '%s'"); public static final Message0 INVALID_VARIABLE_NAME_EMPTY = new Message0( "A variable name cannot be null or empty"); + public static final Message2 ERROR_COMPILE_EXPR_DT_FUNCTION_RULE_IDX = new Message2( "Error compiling output expression in decision table FEEL function, rule index %s: '%s'"); public static String createMessage( Message0 message) { return Msg.buildMessage(message); diff --git a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/CompileEvaluateTest.java b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/CompileEvaluateTest.java index b4a0e3244c4..ddcaec03946 100644 --- a/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/CompileEvaluateTest.java +++ b/kie-dmn/kie-dmn-feel/src/test/java/org/kie/dmn/feel/lang/examples/CompileEvaluateTest.java @@ -12,8 +12,9 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collector; import java.util.stream.Collectors; -import java.util.stream.Stream; - +import java.util.stream.Stream;import org.hamcrest.core.IsNull; +import org.junit.After; +import org.junit.Before; import org.junit.Test; import org.kie.dmn.api.feel.runtime.events.FEELEvent; import org.kie.dmn.api.feel.runtime.events.FEELEvent.Severity; @@ -23,6 +24,7 @@ import org.kie.dmn.feel.lang.CompilerContext; import org.kie.dmn.feel.lang.FEELProperty; import org.kie.dmn.feel.lang.FEELType; +import org.kie.dmn.feel.lang.impl.MapBackedType; import org.kie.dmn.feel.lang.types.BuiltInType; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -30,6 +32,9 @@ public class CompileEvaluateTest { private static final Logger LOG = LoggerFactory.getLogger(CompileEvaluateTest.class); private static final FEEL feel = FEEL.newInstance(); + private List errors; + private FEELEventListener errorsCountingListener; + static { feel.addListener(evt -> { if (evt.getSeverity() == FEELEvent.Severity.ERROR) { @@ -48,15 +53,23 @@ public class CompileEvaluateTest { } ); } + @Before + public void before() { + errors = new ArrayList<>(); + errorsCountingListener = evt -> { if ( evt.getSeverity() == Severity.ERROR ) { errors.add(evt); } }; + feel.addListener( errorsCountingListener ); + } + + @After + public void after() { + feel.removeListener( errorsCountingListener ); + } + @Test public void test_isDynamicResolution() { CompilerContext ctx = feel.newCompilerContext(); ctx.addInputVariableType( "Person List", BuiltInType.LIST); - final List errors = new ArrayList<>(); - FEELEventListener errorsCountingListener = evt -> { if ( evt.getSeverity() == Severity.ERROR ) { errors.add(evt); } }; - feel.addListener( errorsCountingListener ); - CompiledExpression compiledExpression = feel.compile( "Person List[My Variable 1 = \"A\"]", ctx ); assertThat(errors.toString(), errors.size(), is(0) ); @@ -76,7 +89,37 @@ public void test_isDynamicResolution() { assertThat( result, instanceOf( List.class ) ); assertThat( (List) result, hasSize(1) ); assertThat( ((Map) ((List) result).get(0)).get("Full Name"), is("Edson Tirelli") ); + } + + @Test + public void test2() { + CompilerContext ctx = feel.newCompilerContext(); + ctx.addInputVariableType( "MyPerson", new MapBackedType().addField( "FullName", BuiltInType.STRING ) ); - feel.removeListener( errorsCountingListener ); + CompiledExpression compiledExpression = feel.compile( "MyPerson.fullName", ctx ); + + assertThat(errors.toString(), errors.size(), is(1) ); + + Map inputs = new HashMap<>(); + inputs.put( "MyPerson", prototype(entry("FullName", "John Doe")) ); + + Object result = feel.evaluate(compiledExpression, inputs); + + assertThat(result, nullValue()); + } + + @Test + public void test2OK() { + CompilerContext ctx = feel.newCompilerContext(); + ctx.addInputVariableType( "MyPerson", new MapBackedType().addField( "FullName", BuiltInType.STRING ) ); + + CompiledExpression compiledExpression = feel.compile( "MyPerson.FullName", ctx ); + + Map inputs = new HashMap<>(); + inputs.put( "MyPerson", prototype(entry("FullName", "John Doe")) ); + + Object result = feel.evaluate(compiledExpression, inputs); + + assertThat(result, is("John Doe")); } } diff --git a/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/ValidatorTest.java b/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/ValidatorTest.java index 5a9f7c54100..4be75d8ad5f 100644 --- a/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/ValidatorTest.java +++ b/kie-dmn/kie-dmn-validation/src/test/java/org/kie/dmn/validation/ValidatorTest.java @@ -240,11 +240,13 @@ public void testUNKNOWN_VARIABLE() { @Test public void testVALIDATION() { List validate = validator.validate( getReader( "validation.dmn" ), VALIDATE_MODEL, VALIDATE_COMPILATION); - assertThat( ValidatorUtil.formatMessages( validate ), validate.size(), is( 4 ) ); + assertThat( ValidatorUtil.formatMessages( validate ), validate.size(), is( 5 ) ); assertTrue( validate.stream().anyMatch( p -> p.getMessageType().equals( DMNMessageType.INVALID_NAME ) ) ); assertTrue( validate.stream().anyMatch( p -> p.getMessageType().equals( DMNMessageType.MISSING_TYPE_REF ) ) ); assertTrue( validate.stream().anyMatch( p -> p.getMessageType().equals( DMNMessageType.MISSING_EXPRESSION ) ) ); assertTrue( validate.stream().anyMatch( p -> p.getMessageType().equals( DMNMessageType.ERR_COMPILING_FEEL ) ) ); + // on node DTI the `Loan Payment` is of type `tLoanPayment` hence the property is `monthlyAmount`, NOT `amount` as reported in the model FEEL expression: (Loan Payment.amount+... + assertTrue( validate.stream().anyMatch( p -> p.getMessageType().equals( DMNMessageType.ERR_COMPILING_FEEL ) ) ); } }