diff --git a/easy-rules-aviator/pom.xml b/easy-rules-aviator/pom.xml new file mode 100644 index 00000000..8a51d004 --- /dev/null +++ b/easy-rules-aviator/pom.xml @@ -0,0 +1,102 @@ + + + 4.0.0 + + org.jeasy + easy-rules + 4.1.1-SNAPSHOT + + + easy-rules-aviator + jar + Easy Rules Aviator module + Aviator integration module + + + 5.4.1 + + + + git@github.com:j-easy/easy-rules.git + scm:git:git@github.com:j-easy/easy-rules.git + scm:git:git@github.com:j-easy/easy-rules.git + HEAD + + + + GitHub + https://github.com/j-easy/easy-rules/issues + + + + Github Actions + https://github.com/j-easy/easy-rules/actions + + + + + benas + Mahmoud Ben Hassine + http://benas.github.io + mahmoud.benhassine@icloud.com + + Lead developer + + + + + + + MIT License + http://opensource.org/licenses/mit-license.php + + + + + + + org.jeasy + easy-rules-support + ${project.version} + + + com.googlecode.aviator + aviator + ${aviator-version} + + + + org.slf4j + slf4j-simple + test + + + junit + junit + test + + + org.assertj + assertj-core + test + + + com.github.stefanbirkner + system-lambda + ${system-lambda.version} + test + + + + + + + com.mycila + license-maven-plugin + +
${project.parent.basedir}/licence-header-template.txt
+
+
+
+
+
diff --git a/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorAction.java b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorAction.java new file mode 100644 index 00000000..43f54116 --- /dev/null +++ b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorAction.java @@ -0,0 +1,63 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import com.googlecode.aviator.Expression; +import org.jeasy.rules.api.Action; +import org.jeasy.rules.api.Facts; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jeasy.rules.aviator.AviatorRule.DEFAULT_AVIATOR; + +/** + * author peterxiemin(bbcoder1987@gmail.com) + */ +public class AviatorAction implements Action { + + public static final Logger LOGGER = LoggerFactory.getLogger(AviatorAction.class.getName()); + private final String expression; + + private final Expression compileExpression; + + /** + * Create a new {@link AviatorAction}. + * + * @param expression the action written in expression language + */ + public AviatorAction(String expression) { + this.expression = expression; + this.compileExpression = DEFAULT_AVIATOR.compile(expression); + } + + @Override + public void execute(Facts facts) throws Exception { + try { + compileExpression.execute(facts.asMap()); + } catch (Exception e) { + LOGGER.error("Unable to evaluate expression: '" + expression + "' on facts: " + facts); + throw e; + } + } +} diff --git a/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorCondition.java b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorCondition.java new file mode 100644 index 00000000..c81508f3 --- /dev/null +++ b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorCondition.java @@ -0,0 +1,64 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import com.googlecode.aviator.Expression; +import org.jeasy.rules.api.Condition; +import org.jeasy.rules.api.Facts; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.jeasy.rules.aviator.AviatorRule.DEFAULT_AVIATOR; + +/** + * author peterxiemin(bbcoder1987@gmail.com) + */ +public class AviatorCondition implements Condition { + private static final Logger LOGGER = LoggerFactory.getLogger(AviatorCondition.class.getName()); + + private final String expression; + + private final Expression compileExpression; + + + /** + * Create a new {@link AviatorCondition}. + * @param expression + * the condition written in expression language + */ + public AviatorCondition(String expression) { + this.expression = expression; + this.compileExpression = DEFAULT_AVIATOR.compile(expression); + } + + /** + * Evaluate the rule. + * @param facts known when evaluating the rule. + * @return true if the rule should be applied, false otherwise. + */ + @Override + public boolean evaluate(Facts facts) { + return (Boolean) compileExpression.execute(facts.asMap()); + } +} diff --git a/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorRule.java b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorRule.java new file mode 100644 index 00000000..d7605f72 --- /dev/null +++ b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorRule.java @@ -0,0 +1,131 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import com.googlecode.aviator.AviatorEvaluator; +import com.googlecode.aviator.AviatorEvaluatorInstance; +import com.googlecode.aviator.runtime.JavaMethodReflectionFunctionMissing; +import org.jeasy.rules.api.Action; +import org.jeasy.rules.api.Condition; +import org.jeasy.rules.api.Facts; +import org.jeasy.rules.api.Rule; +import org.jeasy.rules.core.BasicRule; + +import java.util.ArrayList; +import java.util.List; + +/** + * author peterxiemin(bbcoder1987@gmail.com) + */ +public class AviatorRule extends BasicRule { + public static final AviatorEvaluatorInstance DEFAULT_AVIATOR; + + static { + DEFAULT_AVIATOR = AviatorEvaluator.getInstance(); + DEFAULT_AVIATOR.setFunctionMissing(JavaMethodReflectionFunctionMissing.getInstance()); + } + + private Condition condition = Condition.FALSE; + + private final List actions = new ArrayList<>(); + + /** + * Create a new Aviator rule. + */ + public AviatorRule() { + super(Rule.DEFAULT_NAME, Rule.DEFAULT_DESCRIPTION, Rule.DEFAULT_PRIORITY); + } + + /** + * set rule name + * @param name + * @return this rule + */ + public AviatorRule name(String name) { + this.name = name; + return this; + } + + /** + * set rule description + * @param description + * @return this rule + */ + public AviatorRule description(String description) { + this.description = description; + return this; + } + + /** + * set rule priority + * @param priority + * @return this rule + */ + public AviatorRule priority(int priority) { + this.priority = priority; + return this; + } + + /** + * set rule condition + * @param condition + * @return this rule + */ + public AviatorRule when(String condition) { + this.condition = new AviatorCondition(condition); + return this; + } + + /** + * set rule action + * @param action + * @return this rule + */ + public AviatorRule then(String action) { + actions.add(new AviatorAction(action)); + return this; + } + + /** + * evaluate the rule + * @param facts + * @return + */ + @Override + public boolean evaluate(Facts facts) { + return condition.evaluate(facts); + } + + /** + * execute the rule + * @param facts + * @throws Exception + */ + @Override + public void execute(Facts facts) throws Exception { + for (Action action : actions) { + action.execute(facts); + } + } +} diff --git a/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorRuleFactory.java b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorRuleFactory.java new file mode 100644 index 00000000..e9b3a1af --- /dev/null +++ b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/AviatorRuleFactory.java @@ -0,0 +1,106 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import org.jeasy.rules.api.Rule; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.support.AbstractRuleFactory; +import org.jeasy.rules.support.RuleDefinition; +import org.jeasy.rules.support.reader.JsonRuleDefinitionReader; +import org.jeasy.rules.support.reader.RuleDefinitionReader; +import org.jeasy.rules.support.reader.YamlRuleDefinitionReader; + +import java.io.Reader; +import java.util.List; + +/** + * author peterxiemin(bbcoder1987@gmail.com) + */ +public class AviatorRuleFactory extends AbstractRuleFactory { + private final RuleDefinitionReader reader; + + /** + * Create a new {@link AviatorRuleFactory} with a given reader. + * + * @param reader used to read rule definitions + * @see YamlRuleDefinitionReader + * @see JsonRuleDefinitionReader + */ + public AviatorRuleFactory(RuleDefinitionReader reader) { + this.reader = reader; + } + + + /** + * Create a new {@link AviatorRule} from a Reader. + * + * The rule descriptor should contain a single rule definition. + * If no rule definitions are found, a {@link IllegalArgumentException} will be thrown. + * If more than a rule is defined in the descriptor, the first rule will be returned. + * + * @param ruleDescriptor descriptor of rule definition + * @return a new rule + * @throws Exception if unable to create the rule from the descriptor + */ + public Rule createRule(Reader ruleDescriptor) throws Exception { + List ruleDefinitions = reader.read(ruleDescriptor); + if (ruleDefinitions.isEmpty()) { + throw new IllegalArgumentException("rule descriptor is empty"); + } + return createRule(ruleDefinitions.get(0)); + } + + /** + * Create a new {@link AviatorRule} from a {@link RuleDefinition}. + * + * @param rulesDescriptor definition of the rule + * @return a new rule + */ + public Rules createRules(Reader rulesDescriptor) throws Exception { + Rules rules = new Rules(); + List ruleDefinitions = reader.read(rulesDescriptor); + for (RuleDefinition ruleDefinition : ruleDefinitions) { + rules.register(createRule(ruleDefinition)); + } + return rules; + } + + /** + * Create a simple rule from a rule definition. + * @param ruleDefinition + * @return + */ + @Override + protected Rule createSimpleRule(RuleDefinition ruleDefinition) { + AviatorRule aviatorRule = new AviatorRule() + .name(ruleDefinition.getName()) + .description(ruleDefinition.getDescription()) + .priority(ruleDefinition.getPriority()) + .when(ruleDefinition.getCondition()); + for (String action : ruleDefinition.getActions()) { + aviatorRule.then(action); + } + return aviatorRule; + } +} diff --git a/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/package-info.java b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/package-info.java new file mode 100644 index 00000000..d6db006e --- /dev/null +++ b/easy-rules-aviator/src/main/java/org/jeasy/rules/aviator/package-info.java @@ -0,0 +1,27 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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. + */ +/** + * This package contains classes to support MVEL. + */ +package org.jeasy.rules.aviator; diff --git a/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorActionTest.java b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorActionTest.java new file mode 100644 index 00000000..7e450510 --- /dev/null +++ b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorActionTest.java @@ -0,0 +1,71 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import org.jeasy.rules.api.Facts; +import org.junit.Test; + +import static com.github.stefanbirkner.systemlambda.SystemLambda.tapSystemOutNormalized; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.AssertionsForClassTypes.assertThatThrownBy; + +/** + * Author peterxiemin(bbcoder1987@gmail.com) + * Tests for {@link AviatorAction}. + */ +public class AviatorActionTest { + @Test + public void shouldExecuteExpressionSuccessfully() throws Exception { + Person person = new Person("foo", 20); + Facts facts = new Facts(); + facts.put("person", person); + AviatorAction personAction = new AviatorAction("setAdult(person, true);"); + personAction.execute(facts); + + assertThat(person.isAdult()).isTrue(); + } + + @Test + public void shouldThrowExceptionWhenExpressionEvaluationFails() { + Person person = new Person("foo", 20); + Facts facts = new Facts(); + facts.put("person", person); + AviatorAction aviatorAction = new AviatorAction("setBlah(person);"); + + assertThatThrownBy(() -> aviatorAction.execute(facts)) + .isInstanceOf(Exception.class) + .hasMessageContaining("No matching method setBlah found taking 0 args for class org.jeasy.rules.aviator.Person"); + } + + @Test + public void shouldExecuteExpressionWithCustomFunction() throws Exception { + Facts facts = new Facts(); + AviatorAction aviatorAction = new AviatorAction("fn hello() { println('Hello from EasyRules&Aviator!'); }; " + + "hello();"); + String output = tapSystemOutNormalized( + () -> aviatorAction.execute(facts)); + aviatorAction.execute(facts); + assertThat(output).isEqualTo("Hello from EasyRules&Aviator!\n"); + } +} \ No newline at end of file diff --git a/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorConditionTest.java b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorConditionTest.java new file mode 100644 index 00000000..b7caa4e7 --- /dev/null +++ b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorConditionTest.java @@ -0,0 +1,70 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import org.jeasy.rules.api.Facts; +import org.junit.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +public class AviatorConditionTest { + @Test + public void testAviatorExpressionEvaluation() { + // given + AviatorCondition isAdult = new AviatorCondition("person.age > 18"); + Facts facts = new Facts(); + facts.put("person", new Person("foo", 20)); + + // when + boolean evaluationResult = isAdult.evaluate(facts); + + // then + assertThat(evaluationResult).isTrue(); + } + + @Test(expected = RuntimeException.class) + public void whenDeclaredFactIsNotPresent_thenShouldThrowRuntimeException() { + // given + AviatorCondition isHot = new AviatorCondition("temperature > 30"); + Facts facts = new Facts(); + + // when + boolean evaluationResult = isHot.evaluate(facts); + + // then + // expected exception + } + + @Test + public void testAviatorExpressionEvaluationWithJavaUtilClass() { + AviatorCondition aviatorCondition = new AviatorCondition("let d = new java.util.Random(123); " + + "return nextBoolean(d)" + + ";"); + + boolean evaluationResult = aviatorCondition.evaluate(new Facts()); + + assertThat(evaluationResult).isTrue(); + } + +} \ No newline at end of file diff --git a/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorRuleFactoryTest.java b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorRuleFactoryTest.java new file mode 100644 index 00000000..256f3817 --- /dev/null +++ b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorRuleFactoryTest.java @@ -0,0 +1,178 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import org.assertj.core.api.Assertions; +import org.jeasy.rules.api.Rule; +import org.jeasy.rules.api.Rules; +import org.jeasy.rules.support.composite.UnitRuleGroup; +import org.jeasy.rules.support.reader.JsonRuleDefinitionReader; +import org.jeasy.rules.support.reader.YamlRuleDefinitionReader; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.File; +import java.io.FileReader; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; + +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(Parameterized.class) +public class AviatorRuleFactoryTest { + @Parameterized.Parameter(0) + public AviatorRuleFactory factory; + @Parameterized.Parameter(1) + public String fileExtension; + + @Parameterized.Parameters + public static Collection parameters() { + return Arrays.asList(new Object[][]{ + {new AviatorRuleFactory(new YamlRuleDefinitionReader()), "yml"}, + {new AviatorRuleFactory(new JsonRuleDefinitionReader()), "json"}, + }); + } + + @Test + public void testCreateRules() throws Exception { + // given + String rulesDescriptor = "src/test/resources/rules." + fileExtension; + // when + Rules rules = factory.createRules(new FileReader(rulesDescriptor)); + // then + Assertions.assertThat(rules).hasSize(2); + Iterator iterator = rules.iterator(); + + Rule rule = iterator.next(); + Assertions.assertThat(rule).isNotNull(); + Assertions.assertThat(rule.getName()).isEqualTo("adult rule"); + Assertions.assertThat(rule.getDescription()).isEqualTo("when age is greater than 18, then mark as adult"); + Assertions.assertThat(rule.getPriority()).isEqualTo(1); + + rule = iterator.next(); + Assertions.assertThat(rule).isNotNull(); + Assertions.assertThat(rule.getName()).isEqualTo("weather rule"); + Assertions.assertThat(rule.getDescription()).isEqualTo("when it rains, then take an umbrella"); + Assertions.assertThat(rule.getPriority()).isEqualTo(2); + } + + @Test + public void testRuleCreationFromFileReader() throws Exception { + // given + Reader adultRuleDescriptorAsReader = new FileReader("src/test/resources/adult-rule." + fileExtension); + + // when + Rule adultRule = factory.createRule(adultRuleDescriptorAsReader); + + // then + assertThat(adultRule.getName()).isEqualTo("adult rule"); + assertThat(adultRule.getDescription()).isEqualTo("when age is greater than 18, then mark as adult"); + assertThat(adultRule.getPriority()).isEqualTo(1); + } + + @Test + public void testRuleCreationFromStringReader() throws Exception { + // given + Path ruleDescriptor = Paths.get("src/test/resources/adult-rule." + fileExtension); + Reader adultRuleDescriptorAsReader = new StringReader(new String(Files.readAllBytes(ruleDescriptor))); + + // when + Rule adultRule = factory.createRule(adultRuleDescriptorAsReader); + + // then + assertThat(adultRule.getName()).isEqualTo("adult rule"); + assertThat(adultRule.getDescription()).isEqualTo("when age is greater than 18, then mark as adult"); + assertThat(adultRule.getPriority()).isEqualTo(1); + } + + @Test + public void testRuleCreationFromFileReader_withCompositeRules() throws Exception { + // given + File rulesDescriptor = new File("src/test/resources/composite-rules." + fileExtension); + + // when + Rules rules = factory.createRules(new FileReader(rulesDescriptor)); + + // then + assertThat(rules).hasSize(2); + Iterator iterator = rules.iterator(); + + Rule rule = iterator.next(); + assertThat(rule).isNotNull(); + assertThat(rule.getName()).isEqualTo("Movie id rule"); + assertThat(rule.getDescription()).isEqualTo("description"); + assertThat(rule.getPriority()).isEqualTo(1); + assertThat(rule).isInstanceOf(UnitRuleGroup.class); + + rule = iterator.next(); + assertThat(rule).isNotNull(); + assertThat(rule.getName()).isEqualTo("weather rule"); + assertThat(rule.getDescription()).isEqualTo("when it rains, then take an umbrella"); + assertThat(rule.getPriority()).isEqualTo(1); + } + + @Test + public void testRuleCreationFromFileReader_withInvalidCompositeRuleType() { + // given + File rulesDescriptor = new File("src/test/resources/composite-rule-invalid-composite-rule-type." + fileExtension); + + // when + Assertions.assertThatThrownBy(() -> factory.createRule(new FileReader(rulesDescriptor))) + // then + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Invalid composite rule type, must be one of [UnitRuleGroup, ConditionalRuleGroup, ActivationRuleGroup]"); + } + + @Test + public void testRuleCreationFromFileReader_withEmptyComposingRules() { + // given + File rulesDescriptor = new File("src/test/resources/composite-rule-invalid-empty-composing-rules." + fileExtension); + + // when + Assertions.assertThatThrownBy(() -> factory.createRule(new FileReader(rulesDescriptor))) + // then + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Composite rules must have composing rules specified"); + } + + @Test + public void testRuleCreationFromFileReader_withNonCompositeRuleDeclaresComposingRules() { + // given + File rulesDescriptor = new File("src/test/resources/non-composite-rule-with-composing-rules." + fileExtension); + + // when + Assertions.assertThatThrownBy(() -> factory.createRule(new FileReader(rulesDescriptor))) + // then + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("Non-composite rules cannot have composing rules"); + } + +} \ No newline at end of file diff --git a/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorRuleTest.java b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorRuleTest.java new file mode 100644 index 00000000..6ce944a6 --- /dev/null +++ b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/AviatorRuleTest.java @@ -0,0 +1,68 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +import org.jeasy.rules.api.Facts; +import org.junit.Before; +import org.junit.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +public class AviatorRuleTest { + private Facts facts = new Facts(); + + private AviatorRule aviatorRule = new AviatorRule().name("rn").description("rd").priority(1); + + @Before + public void setUp() { + aviatorRule.when("person.age > 18"); + aviatorRule.then("setAdult(person, true);"); + } + + @Test + public void whenTheRuleIsTriggered_thenConditionShouldBeEvaluated() { + // given + facts.put("person", new Person("foo", 20)); + + // when + boolean evaluationResult = aviatorRule.evaluate(facts); + + // then + assertThat(evaluationResult).isTrue(); + } + + @Test + public void whenTheConditionIsTrue_thenActionsShouldBeExecuted() throws Exception { + // given + Person foo = new Person("foo", 20); + facts.put("person", foo); + + // when + aviatorRule.execute(facts); + + // then + assertThat(foo.isAdult()).isTrue(); + } + +} diff --git a/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/Person.java b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/Person.java new file mode 100644 index 00000000..18714304 --- /dev/null +++ b/easy-rules-aviator/src/test/java/org/jeasy/rules/aviator/Person.java @@ -0,0 +1,60 @@ +/* + * The MIT License + * + * Copyright (c) 2021, 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.aviator; + +public class Person { + + private String name; + private int age; + private boolean isAdult; + + public Person(String name, int age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public boolean isAdult() { + return isAdult; + } + + public void setAdult(boolean adult) { + isAdult = adult; + } +} diff --git a/easy-rules-aviator/src/test/resources/adult-rule.json b/easy-rules-aviator/src/test/resources/adult-rule.json new file mode 100644 index 00000000..06284c1c --- /dev/null +++ b/easy-rules-aviator/src/test/resources/adult-rule.json @@ -0,0 +1,11 @@ +[ + { + "name": "adult rule", + "description": "when age is greater than 18, then mark as adult", + "priority": 1, + "condition": "person.age > 18", + "actions": [ + "setAdult(person, true);" + ] + } +] diff --git a/easy-rules-aviator/src/test/resources/adult-rule.yml b/easy-rules-aviator/src/test/resources/adult-rule.yml new file mode 100644 index 00000000..1660bf89 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/adult-rule.yml @@ -0,0 +1,6 @@ +name: adult rule +description: when age is greater than 18, then mark as adult +priority: 1 +condition: "person.age > 18" +actions: + - "setAdult(person, true);" diff --git a/easy-rules-aviator/src/test/resources/composite-rule-invalid-composite-rule-type.json b/easy-rules-aviator/src/test/resources/composite-rule-invalid-composite-rule-type.json new file mode 100644 index 00000000..87dfdd28 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/composite-rule-invalid-composite-rule-type.json @@ -0,0 +1,23 @@ +[ + { + "name": "invalid rule", + "compositeRuleType": "foo", + "priority": 1, + "composingRules": [ + { + "name": "rule1", + "condition": "true", + "actions": [ + "p(\"\");" + ] + }, + { + "name": "rule2", + "condition": "false", + "actions": [ + "p(\"\");" + ] + } + ] + } +] diff --git a/easy-rules-aviator/src/test/resources/composite-rule-invalid-composite-rule-type.yml b/easy-rules-aviator/src/test/resources/composite-rule-invalid-composite-rule-type.yml new file mode 100644 index 00000000..f2b1ef16 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/composite-rule-invalid-composite-rule-type.yml @@ -0,0 +1,12 @@ +name: invalid rule +compositeRuleType: foo +priority: 1 +composingRules: + - name: rule1 + condition: "true" + actions: + - "p(\"\");" + - name: rule2 + condition: "false" + actions: + - "p(\"\");" diff --git a/easy-rules-aviator/src/test/resources/composite-rule-invalid-empty-composing-rules.json b/easy-rules-aviator/src/test/resources/composite-rule-invalid-empty-composing-rules.json new file mode 100644 index 00000000..19389a08 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/composite-rule-invalid-empty-composing-rules.json @@ -0,0 +1,8 @@ +[ + { + "name": "invalid rule", + "compositeRuleType": "UnitRuleGroup", + "priority": 1, + "composingRules": [] + } +] diff --git a/easy-rules-aviator/src/test/resources/composite-rule-invalid-empty-composing-rules.yml b/easy-rules-aviator/src/test/resources/composite-rule-invalid-empty-composing-rules.yml new file mode 100644 index 00000000..b6ad31e9 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/composite-rule-invalid-empty-composing-rules.yml @@ -0,0 +1,4 @@ +name: invalid rule +compositeRuleType: UnitRuleGroup +priority: 1 +composingRules: diff --git a/easy-rules-aviator/src/test/resources/composite-rules.json b/easy-rules-aviator/src/test/resources/composite-rules.json new file mode 100644 index 00000000..69b5b5a7 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/composite-rules.json @@ -0,0 +1,36 @@ +[ + { + "name": "Movie id rule", + "compositeRuleType": "UnitRuleGroup", + "priority": 1, + "composingRules": [ + { + "name": "Time is evening", + "description": "If it's later than 7pm", + "priority": 1, + "condition": "day.hour > 19", + "actions": [ + "shouldProvideId(person, true);" + ] + }, + { + "name": "Movie is rated R", + "description": "If the movie is rated R", + "priority": 1, + "condition": "movie.rating == R", + "actions": [ + "shouldProvideId(person, true);" + ] + } + ] + }, + { + "name": "weather rule", + "description": "when it rains, then take an umbrella", + "priority": 1, + "condition": "rain == True", + "actions": [ + "p(\"It rains, take an umbrella!\");" + ] + } +] \ No newline at end of file diff --git a/easy-rules-aviator/src/test/resources/composite-rules.yml b/easy-rules-aviator/src/test/resources/composite-rules.yml new file mode 100644 index 00000000..f2806084 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/composite-rules.yml @@ -0,0 +1,23 @@ +name: Movie id rule +compositeRuleType: UnitRuleGroup +priority: 1 +composingRules: + - name: Time is evening + description: If it's later than 7pm + priority: 1 + condition: "day.hour > 19" + actions: + - "shouldProvideId(person, true);" + - name: Movie is rated R + description: If the movie is rated R + priority: 1 + condition: "movie.rating == R" + actions: + - "shouldProvideId(person, true);" +--- +name: weather rule +description: when it rains, then take an umbrella +priority: 1 +condition: "rain == True" +actions: + - "p(\"It rains, take an umbrella!\");" diff --git a/easy-rules-aviator/src/test/resources/non-composite-rule-with-composing-rules.json b/easy-rules-aviator/src/test/resources/non-composite-rule-with-composing-rules.json new file mode 100644 index 00000000..2d299033 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/non-composite-rule-with-composing-rules.json @@ -0,0 +1,30 @@ +[ + { + "name": "Movie id rule", + "priority": 1, + "condition": "true", + "actions": [ + "p(\"\");" + ], + "composingRules": [ + { + "name": "Time is evening", + "description": "If it's later than 7pm", + "priority": 1, + "condition": "day.hour > 19", + "actions": [ + "shouldProvideId(person, true);" + ] + }, + { + "name": "Movie is rated R", + "description": "If the movie is rated R", + "priority": 1, + "condition": "movie.rating == R", + "actions": [ + "shouldProvideId(person, true);" + ] + } + ] + } +] diff --git a/easy-rules-aviator/src/test/resources/non-composite-rule-with-composing-rules.yml b/easy-rules-aviator/src/test/resources/non-composite-rule-with-composing-rules.yml new file mode 100644 index 00000000..b19b8034 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/non-composite-rule-with-composing-rules.yml @@ -0,0 +1,18 @@ +name: Movie id rule +priority: 1 +condition: "true" +actions: + - "p(\"\");" +composingRules: + - name: Time is evening + description: If it's later than 7pm + priority: 1 + condition: "day.hour > 19" + actions: + - "shouldProvideId(person, true);" + - name: Movie is rated R + description: If the movie is rated R + priority: 1 + condition: "movie.rating == R" + actions: + - "shouldProvideId(person, true);" diff --git a/easy-rules-aviator/src/test/resources/rules.json b/easy-rules-aviator/src/test/resources/rules.json new file mode 100644 index 00000000..49b6b413 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/rules.json @@ -0,0 +1,20 @@ +[ + { + "name": "adult rule", + "description": "when age is greater than 18, then mark as adult", + "priority": 1, + "condition": "person.age > 18", + "actions": [ + "setAdult(person, true);" + ] + }, + { + "name": "weather rule", + "description": "when it rains, then take an umbrella", + "priority": 2, + "condition": "rain == true", + "actions": [ + "p(\"It rains, take an umbrella!\");" + ] + } +] diff --git a/easy-rules-aviator/src/test/resources/rules.yml b/easy-rules-aviator/src/test/resources/rules.yml new file mode 100644 index 00000000..3532b1f3 --- /dev/null +++ b/easy-rules-aviator/src/test/resources/rules.yml @@ -0,0 +1,14 @@ +--- +name: adult rule +description: when age is greater than 18, then mark as adult +priority: 1 +condition: "person.age > 18" +actions: + - "setAdult(person, true);" +--- +name: weather rule +description: when it rains, then take an umbrella +priority: 2 +condition: "rain == true" +actions: + - "p(\"It rains, take an umbrella!\");" diff --git a/pom.xml b/pom.xml index 426ac55c..24b32f69 100644 --- a/pom.xml +++ b/pom.xml @@ -20,6 +20,7 @@ easy-rules-support easy-rules-spel easy-rules-jexl + easy-rules-aviator pom @@ -168,7 +169,7 @@ ${maven-license-plugin.version} - 2020 + 2021 true