Skip to content

[incubator-kie-issues#2021] DMN boxed filter behavior in relation to implicit conversions #6393

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Jul 17, 2025
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -46,35 +46,42 @@ public abstract class DMNTypeRegistryAbstract implements DMNTypeRegistry, FEELTy
protected ScopeImpl feelTypesScope = new ScopeImpl(); // no parent scope, intentional.
protected Map<TupleIdentifier, ScopeImpl> feelTypesScopeChildLU = new HashMap<>();


public DMNTypeRegistryAbstract(Map<TupleIdentifier, QName> aliases) {
this.aliases = aliases;
String feelNamespace = feelNS();
Map<String, DMNType> feelTypes = new HashMap<>( );
Map<String, DMNType> feelTypes = new HashMap<>();
types.put( feelNamespace, feelTypes );
DMNType unknownType = unknown();
this.feelTypesScope = getScopeImpl(feelNamespace, unknownType, feelTypes);
}

for (String name : BuiltInType.UNKNOWN.getNames()) {
feelTypes.put(name, unknown());
feelTypesScope.define(new TypeSymbol(name, BuiltInType.UNKNOWN));
}

static ScopeImpl getScopeImpl(String feelNamespace, DMNType unknownType, Map<String, DMNType> feelTypes) {
ScopeImpl feelTypesScope = new ScopeImpl();
for( BuiltInType type : BuiltInType.values() ) {
boolean isUnknown = type == BuiltInType.UNKNOWN;
for( String name : type.getNames() ) {
DMNType feelPrimitiveType;
if( type == BuiltInType.UNKNOWN ) {
// already added, skip it
continue;
} else if( type == BuiltInType.LIST ) {
feelPrimitiveType = new SimpleTypeImpl(feelNamespace, name, null, false, null, null, unknown(), type);
} else if( type == BuiltInType.CONTEXT ) {
feelPrimitiveType = new CompositeTypeImpl( feelNamespace, name, null, false, Collections.emptyMap(), null, type );
} else {
feelPrimitiveType = new SimpleTypeImpl( feelNamespace, name, null, false, null, null, null, type );
}
feelTypes.put( name, feelPrimitiveType );
feelTypesScope.define(new TypeSymbol(name, type));
manageAllTypes(isUnknown, unknownType, name, type, feelNamespace, feelTypes, feelTypesScope );
}
}
return feelTypesScope;
}

static void manageAllTypes(boolean isUnknown, DMNType unknownType, String name, BuiltInType type, String feelNamespace, Map<String, DMNType> feelTypes, ScopeImpl feelTypesScope ) {
DMNType feelPrimitiveType = isUnknown ? unknownType : getFeelPrimitiveType(name, type, feelNamespace, unknownType);
feelTypes.put( name, feelPrimitiveType );
feelTypesScope.define(new TypeSymbol(name, type));
}

static DMNType getFeelPrimitiveType(String name, BuiltInType type, String feelNamespace, DMNType unknownType) {
DMNType feelPrimitiveType;
if( type == BuiltInType.LIST ) {
feelPrimitiveType = new SimpleTypeImpl(feelNamespace, name, null, true, null, null, unknownType, type);
} else if( type == BuiltInType.CONTEXT ) {
feelPrimitiveType = new CompositeTypeImpl( feelNamespace, name, null, false, Collections.emptyMap(), null, type );
} else {
feelPrimitiveType = new SimpleTypeImpl( feelNamespace, name, null, false, null, null, null, type );
}
return feelPrimitiveType;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,18 @@
import java.time.ZonedDateTime;
import java.time.chrono.ChronoPeriod;
import java.time.temporal.ChronoUnit;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;

import java.util.UUID;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Timeout;
import org.junit.jupiter.params.ParameterizedTest;
Expand All @@ -62,7 +63,6 @@
import org.kie.dmn.api.core.event.BeforeEvaluateDecisionEvent;
import org.kie.dmn.api.core.event.BeforeEvaluateDecisionTableEvent;
import org.kie.dmn.api.core.event.DMNRuntimeEventListener;
import org.kie.dmn.api.feel.runtime.events.FEELEvent;
import org.kie.dmn.core.api.DMNFactory;
import org.kie.dmn.api.core.EvaluatorResult;
import org.kie.dmn.core.ast.DMNContextEvaluator;
Expand All @@ -77,7 +77,6 @@
import org.kie.dmn.feel.lang.types.BuiltInType;
import org.kie.dmn.feel.lang.types.impl.ComparablePeriod;
import org.kie.dmn.feel.marshaller.FEELStringMarshaller;
import org.kie.dmn.feel.runtime.events.FEELEventBase;
import org.kie.dmn.feel.util.BuiltInTypeUtils;
import org.kie.dmn.feel.util.Msg;
import org.kie.dmn.feel.util.NumberEvalHelper;
Expand Down Expand Up @@ -463,6 +462,112 @@ void boxedContext(boolean useExecModelCompiler) {
assertThat((Map<String, Object>) dmnResult.getContext().get("Math")).containsEntry("Product", BigDecimal.valueOf(50));
}

@ParameterizedTest
@MethodSource("params")
void boxedFilter(boolean useExecModelCompiler) {
init(useExecModelCompiler);
final DMNRuntime runtime = DMNRuntimeUtil.createRuntime("valid_models/DMNv1_6/BoxedFilter.dmn", this.getClass());
final DMNModel dmnModel = runtime.getModel(
"https://kie.org/dmn/_42BAF435-BAF7-460A-8C5D-351ABB98403C",
"DMN_DFD7A647-6D67-4014-834B-E7559668C4B5"
);
assertThat(dmnModel).isNotNull();
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();

final DMNContext context = DMNFactory.newContext();
context.set("fruit", "Mango");

final DMNResult dmnResult = runtime.evaluateAll(dmnModel, context);
assertThat(dmnResult).isNotNull();
assertThat(dmnResult.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnResult.getMessages())).isFalse();
assertThat(dmnResult.getContext().get("BoxedFilter")).isEqualTo(Collections.singletonList("Mango"));
}

@ParameterizedTest
@MethodSource("params")
void boxedFilterInvalidCondition(boolean useExecModelCompiler) {
init(useExecModelCompiler);
final DMNRuntime runtime = DMNRuntimeUtil.createRuntime("valid_models/DMNv1_6/BoxedFilter.dmn", this.getClass());
final DMNModel dmnModel = runtime.getModel(
"https://kie.org/dmn/_42BAF435-BAF7-460A-8C5D-351ABB98403C",
"DMN_DFD7A647-6D67-4014-834B-E7559668C4B5"
);
assertThat(dmnModel).isNotNull();
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();

final DMNContext context = DMNFactory.newContext();
context.set("fruit", "Kiwi");

final DMNResult dmnResult = runtime.evaluateAll(dmnModel, context);
assertThat(dmnResult).isNotNull();
assertThat(dmnResult.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnResult.getMessages())).isFalse();
assertThat(dmnResult.getContext().get("BoxedFilter")).isEqualTo(Collections.emptyList());
}

@ParameterizedTest
@MethodSource("params")
void boxedIterator(boolean useExecModelCompiler) {
init(useExecModelCompiler);
final DMNRuntime runtime = DMNRuntimeUtil.createRuntime("valid_models/DMNv1_6/BoxedIterator.dmn", this.getClass());
final DMNModel dmnModel = runtime.getModel(
"https://kie.org/dmn/_60C30701-AFB7-49C5-9B23-A0167DDC6795",
"DMN_A15DEB5A-841F-4AC0-8C09-2403F5707373"
);
assertThat(dmnModel).isNotNull();
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();

final DMNContext context = DMNFactory.newContext();
context.set("singleNumber", 5);
final DMNResult dmnResult = runtime.evaluateAll(dmnModel, context);
assertThat(dmnResult).isNotNull();
assertThat(dmnResult.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnResult.getMessages())).isFalse();
assertThat(dmnResult.getContext().get("BoxedIterator")).isEqualTo(Collections.singletonList(BigDecimal.valueOf(30)));
}

@ParameterizedTest
@MethodSource("params")
void boxedIteratorInvalidConditionWithInvalidInput(boolean useExecModelCompiler) {
init(useExecModelCompiler);
final DMNRuntime runtime = DMNRuntimeUtil.createRuntime("valid_models/DMNv1_6/BoxedIterator.dmn", this.getClass());
final DMNModel dmnModel = runtime.getModel(
"https://kie.org/dmn/_60C30701-AFB7-49C5-9B23-A0167DDC6795",
"DMN_A15DEB5A-841F-4AC0-8C09-2403F5707373"
);
assertThat(dmnModel).isNotNull();
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();

final DMNContext context = DMNFactory.newContext();
context.set("singleNumber", "invalid");

final DMNResult dmnResult = runtime.evaluateAll(dmnModel, context);
assertThat(dmnResult).isNotNull();
assertThat(dmnResult.hasErrors()).isTrue();
assertThat(dmnResult.getContext().get("BoxedIterator")).isNull();
assertThat(dmnResult.getMessages(Severity.ERROR).get(0).getMessageType()).isEqualTo(DMNMessageType.ERROR_EVAL_NODE);
}

@ParameterizedTest
@MethodSource("params")
void boxedIteratorInvalidConditionWithNonNumeric(boolean useExecModelCompiler) {
init(useExecModelCompiler);
final DMNRuntime runtime = DMNRuntimeUtil.createRuntime("valid_models/DMNv1_6/BoxedIterator.dmn", this.getClass());
final DMNModel dmnModel = runtime.getModel(
"https://kie.org/dmn/_60C30701-AFB7-49C5-9B23-A0167DDC6795",
"DMN_A15DEB5A-841F-4AC0-8C09-2403F5707373"
);
assertThat(dmnModel).isNotNull();
assertThat(dmnModel.hasErrors()).as(DMNRuntimeUtil.formatMessages(dmnModel.getMessages())).isFalse();

final DMNContext context = DMNFactory.newContext();
context.set("singleNumber", List.of(1, 2, 3));

final DMNResult dmnResult = runtime.evaluateAll(dmnModel, context);
assertThat(dmnResult).isNotNull();
assertThat(dmnResult.hasErrors()).isTrue();
assertThat(dmnResult.getContext().get("BoxedIterator")).isNull();
assertThat(dmnResult.getMessages(Severity.ERROR).get(0).getMessageType()).isEqualTo(DMNMessageType.ERROR_EVAL_NODE);
}

@ParameterizedTest
@MethodSource("params")
void functionDefAndInvocation(boolean useExecModelCompiler) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.kie.dmn.core.compiler;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.kie.dmn.api.core.DMNType;
import org.kie.dmn.core.impl.SimpleTypeImpl;
import org.kie.dmn.feel.lang.types.BuiltInType;
import org.kie.dmn.feel.lang.types.ScopeImpl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Stream;

public class DMNTypeRegistryAbstractTest {

public static final Logger LOG = LoggerFactory.getLogger(DMNTypeRegistryAbstractTest.class);

@Test
void getScopeImpl() {
String feelNamespace = "feel";
DMNType unknownType = new SimpleTypeImpl(feelNamespace, "unknown", null, false, null, null, null, BuiltInType.UNKNOWN);
Map<String, DMNType> feelTypes = new HashMap<>();
ScopeImpl feelTypesScope = DMNTypeRegistryAbstract.getScopeImpl(feelNamespace, unknownType, feelTypes);

assertThat(feelTypesScope).isNotNull();
assertThat(feelTypesScope.resolve("list").getId()).isEqualTo("list");
assertThat(feelTypesScope.resolve("list").getType()).isEqualTo(BuiltInType.LIST);
}

@Test
void manageAllTypes() {
DMNType unknownType = new SimpleTypeImpl("feel", "unknown", null, false, null, null, null, BuiltInType.UNKNOWN);
ScopeImpl feelTypesScope = new ScopeImpl();
Map<String, DMNType> feelTypes = new HashMap<>();
DMNTypeRegistryAbstract.manageAllTypes(false, unknownType, "list", BuiltInType.LIST, "feel", feelTypes, feelTypesScope);
assertThat(feelTypes).containsKey("list");
}

@ParameterizedTest
@MethodSource("params")
void testAllBuiltInTypes(BuiltInType builtInType, boolean isCollection, String name) {
DMNType result = DMNTypeRegistryAbstract.getFeelPrimitiveType(name, builtInType, "feel", null);

assertThat(result).isNotNull();
assertThat(result.isCollection()).isEqualTo(isCollection);
assertThat(result.getName()).isEqualTo(name);
}

static Stream<Arguments> params() {
return Arrays.stream(BuiltInType.values()).flatMap(type -> Arrays.stream(type.getNames()).map(name -> Arguments.of(type, type == BuiltInType.LIST, name)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->

<definitions xmlns="https://www.omg.org/spec/DMN/20240513/MODEL/"
xmlns:dmndi="https://www.omg.org/spec/DMN/20230324/DMNDI/"
xmlns:dc="http://www.omg.org/spec/DMN/20180521/DC/"
xmlns:di="http://www.omg.org/spec/DMN/20180521/DI/"
xmlns:kie="https://kie.org/dmn/extensions/1.0"
expressionLanguage="https://www.omg.org/spec/DMN/20240513/FEEL/"
namespace="https://kie.org/dmn/_42BAF435-BAF7-460A-8C5D-351ABB98403C"
id="_3AC6172C-DFA4-4A63-A74A-2F44CD4874FC"
name="DMN_DFD7A647-6D67-4014-834B-E7559668C4B5">
<decision name="BoxedFilter" id="_8594A31A-A112-424E-B7DA-16A50FF9F126">
<variable name="BoxedFilter" id="_1FE53B3D-7EC4-48AF-9C09-CC1814FFFB1A" typeRef="list" />
<informationRequirement id="_7DF29C72-BDF0-4360-8B7F-9C7948E299D3">
<requiredInput href="#_6469745D-B5A7-439E-8893-584F5F69EDF1" />
</informationRequirement>
<literalExpression id="_03F7B046-C322-42B3-92C2-765AE21388E9" typeRef="list" label="BoxedFilter">
<text>[fruit][item = &quot;Mango&quot;]</text>
</literalExpression>
</decision>
<inputData name="fruit" id="_6469745D-B5A7-439E-8893-584F5F69EDF1">
<variable name="fruit" id="_35AC7E18-9978-4077-856E-456ADCEEDB5B" typeRef="string" />
</inputData>
<dmndi:DMNDI>
<dmndi:DMNDiagram id="_F2A74F0B-D336-40DC-84CC-CFBE768F10CF" name="Default DRD" useAlternativeInputDataShape="false">
<di:extension>
<kie:ComponentsWidthsExtension>
<kie:ComponentWidths dmnElementRef="_03F7B046-C322-42B3-92C2-765AE21388E9">
<kie:width>190</kie:width>
</kie:ComponentWidths>
</kie:ComponentsWidthsExtension>
</di:extension>
<dmndi:DMNShape id="_714E9689-AF7C-4F9F-890D-E875702283A4" dmnElementRef="_8594A31A-A112-424E-B7DA-16A50FF9F126" isCollapsed="false" isListedInputData="false">
<dc:Bounds x="340" y="60" width="160" height="80" />
</dmndi:DMNShape>
<dmndi:DMNShape id="_510E6D99-11A8-475B-9D76-D9C7D3096EEA" dmnElementRef="_6469745D-B5A7-439E-8893-584F5F69EDF1" isCollapsed="false" isListedInputData="false">
<dc:Bounds x="340" y="280" width="160" height="80" />
</dmndi:DMNShape>
<dmndi:DMNEdge id="_885D05BB-D421-47C4-91AB-25AD654D5A5C" dmnElementRef="_7DF29C72-BDF0-4360-8B7F-9C7948E299D3" sourceElement="_510E6D99-11A8-475B-9D76-D9C7D3096EEA" targetElement="_714E9689-AF7C-4F9F-890D-E875702283A4">
<di:waypoint x="420" y="320" />
<di:waypoint x="420" y="140" />
</dmndi:DMNEdge>
</dmndi:DMNDiagram>
</dmndi:DMNDI>
</definitions>
Loading