diff --git a/easy-rules-core/pom.xml b/easy-rules-core/pom.xml index 7943783..fab3ef9 100644 --- a/easy-rules-core/pom.xml +++ b/easy-rules-core/pom.xml @@ -92,6 +92,12 @@ jsr305 test + + org.jspecify + jspecify + 1.0.0 + test + diff --git a/easy-rules-core/src/main/java/org/jeasy/rules/core/RuleProxy.java b/easy-rules-core/src/main/java/org/jeasy/rules/core/RuleProxy.java index 0819531..9bcba58 100644 --- a/easy-rules-core/src/main/java/org/jeasy/rules/core/RuleProxy.java +++ b/easy-rules-core/src/main/java/org/jeasy/rules/core/RuleProxy.java @@ -29,6 +29,7 @@ import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.Iterator; @@ -175,8 +176,8 @@ private Object compareToMethod(final Object[] args) throws Exception { private List getActualParameters(Method method, Facts facts) { List actualParameters = new ArrayList<>(); - Annotation[][] parameterAnnotations = method.getParameterAnnotations(); - for (Annotation[] annotations : parameterAnnotations) { + for (Parameter parameter : method.getParameters()) { + Annotation[] annotations = parameter.getAnnotations(); String factName = null; boolean annotatedAsNullable = false; for (Annotation annotation : annotations) { @@ -186,6 +187,11 @@ private List getActualParameters(Method method, Facts facts) { annotatedAsNullable = true; } } + for (Annotation annotation : parameter.getAnnotatedType().getAnnotations()) { + if (RulesEngineParameters.hasOptionalParameterAnnotation(annotation.annotationType())) { + annotatedAsNullable = true; + } + } if (factName != null) { Object fact = facts.get(factName); //validated upfront. if (fact == null && !facts.asMap().containsKey(factName) && !annotatedAsNullable) { diff --git a/easy-rules-core/src/test/java/org/jeasy/rules/core/OptionalFactTypeAnnotationParameterTest.java b/easy-rules-core/src/test/java/org/jeasy/rules/core/OptionalFactTypeAnnotationParameterTest.java new file mode 100644 index 0000000..15d5f3f --- /dev/null +++ b/easy-rules-core/src/test/java/org/jeasy/rules/core/OptionalFactTypeAnnotationParameterTest.java @@ -0,0 +1,127 @@ +/* + * The MIT License + * + * Copyright (c) 2022, Mahmoud Ben Hassine (mahmoud.benhassine@icloud.com) + * + * 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: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * 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. + */ +package org.jeasy.rules.core; + +import org.jeasy.rules.annotation.Action; +import org.jeasy.rules.annotation.Condition; +import org.jeasy.rules.annotation.Fact; +import org.jeasy.rules.annotation.Rule; +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.api.RulesEngineParameters; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.jspecify.annotations.Nullable; +import java.util.Map; + +/** Null facts are not accepted by design, a declared fact can be missing though. */ +public class OptionalFactTypeAnnotationParameterTest extends AbstractTest { + + @BeforeEach + public void setup() throws Exception { + facts = new Facts(); + facts.put("fact1", fact1); + facts.put("fact2", fact2); + rules = new Rules(); + + RulesEngineParameters rulesEngineParameters = new RulesEngineParameters(); + rulesEngineParameters.setFailsOnException(true); + rulesEngine = new DefaultRulesEngine(rulesEngineParameters); + } + + @AfterEach + void cleanup() { + RulesEngineParameters.setOptionalParameterAnnotation(null); + } + + @Test + public void testMissingFact() { + RulesEngineParameters.setOptionalParameterAnnotation(Nullable.class); + Rules rules = new Rules(); + rules.register(new AnnotatedParametersRule()); + + Facts facts = new Facts(); + facts.put("fact1", new Object()); + + Map results = rulesEngine.check(rules, facts); + + for (boolean b : results.values()) { + Assertions.assertTrue(b); + } + } + + @Test + public void testMissingFactNotAnnotated() { + + Rules rules = new Rules(); + + new AnnotatedParametersRule(); + rules.register(new AnnotatedParametersRule()); + + Facts facts = new Facts(); + facts.put("fact1", new Object()); + + Map results = rulesEngine.check(rules, facts); + + for (boolean b : results.values()) { + Assertions.assertFalse(b); + } + } + + @Test + public void testNoMissingFact() { + Rules rules = new Rules(); + rules.register( + new AnnotatedParametersRule() { + @Condition + public boolean when(@Fact("fact1") Object fact1, @Nullable @Fact("fact2") Object fact2) { + return fact1 != null && fact2 != null; + } + }); + Facts facts = new Facts(); + facts.put("fact1", new Object()); + facts.put("fact2", new Object()); + + Map results = rulesEngine.check(rules, facts); + + for (boolean b : results.values()) { + Assertions.assertTrue(b); + } + } + + @Rule + public static class AnnotatedParametersRule { + + @Condition + public boolean when(@Fact("fact1") Object fact1, @Nullable @Fact("fact2") Object fact2) { + return fact1 != null && fact2 == null; + } + + @Action + public void then(@Fact("fact1") Object fact1, @Nullable @Fact("fact2") Object fact2) {} + } +}