+ }
+ class LoanApplication {
+ + LoanApplication(age : int, monthlyIncome : double, loanAmount : double, creditScore : int)
+ + age() : int
+ + monthlyIncome() : double
+ + loanAmount() : double
+ + creditScore() : int
+ }
+ class MinimumAgeRule {
+ - minimumAge : int
+ + MinimumAgeRule(minimumAge : int)
+ + name() : String
+ + evaluate(context : LoanApplication) : boolean
+ + execute(context : LoanApplication) : void
+ }
+ class MinimumIncomeRule {
+ - minimumIncome : double
+ + MinimumIncomeRule(minimumIncome : double)
+ + name() : String
+ + evaluate(context : LoanApplication) : boolean
+ + execute(context : LoanApplication) : void
+ }
+ class CreditScoreRule {
+ - minimumScore : int
+ + CreditScoreRule(minimumScore : int)
+ + name() : String
+ + evaluate(context : LoanApplication) : boolean
+ + execute(context : LoanApplication) : void
+ }
+ class App {
+ + App()
+ + main(args : String[]) : void
+ }
+}
+RuleEngine --> "*" Rule
+RuleEngine ..> RuleEngineResult
+MinimumAgeRule ..|> Rule
+MinimumIncomeRule ..|> Rule
+CreditScoreRule ..|> Rule
+MinimumAgeRule ..> LoanApplication
+MinimumIncomeRule ..> LoanApplication
+CreditScoreRule ..> LoanApplication
+@enduml
diff --git a/rule-engine/pom.xml b/rule-engine/pom.xml
new file mode 100644
index 000000000000..29738d783a99
--- /dev/null
+++ b/rule-engine/pom.xml
@@ -0,0 +1,70 @@
+
+
+
+
+ java-design-patterns
+ com.iluwatar
+ 1.26.0-SNAPSHOT
+
+ 4.0.0
+ rule-engine
+
+
+ org.slf4j
+ slf4j-api
+
+
+ ch.qos.logback
+ logback-classic
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-assembly-plugin
+
+
+
+
+
+ com.iluwatar.ruleengine.App
+
+
+
+
+
+
+
+
+
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/App.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/App.java
new file mode 100644
index 000000000000..78a51f152b84
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/App.java
@@ -0,0 +1,69 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import java.util.List;
+import lombok.extern.slf4j.Slf4j;
+
+/**
+ * The Rule Engine pattern encapsulates each business rule as an independent object and lets a
+ * {@link RuleEngine} evaluate a collection of them against a shared context. This replaces large,
+ * tangled conditional blocks with small, testable rules that can be added or removed without
+ * touching the orchestration logic.
+ *
+ * This demo builds a loan approval engine from three rules and runs two applications through it:
+ * one that satisfies every rule and one that fails two of them. Because the engine evaluates every
+ * rule rather than stopping at the first failure, the result reports all the reasons an application
+ * was rejected.
+ */
+@Slf4j
+public class App {
+
+ /**
+ * Program entry point.
+ *
+ * @param args command line arguments
+ */
+ public static void main(String[] args) {
+ var engine =
+ new RuleEngine<>(
+ List.of(
+ new MinimumAgeRule(18), new MinimumIncomeRule(2000.0), new CreditScoreRule(650)));
+
+ var approved = new LoanApplication(30, 3500.0, 15000.0, 720);
+ var rejected = new LoanApplication(17, 1500.0, 15000.0, 720);
+
+ report(engine.run(approved));
+ report(engine.run(rejected));
+ }
+
+ private static void report(RuleEngineResult result) {
+ if (result.approved()) {
+ LOGGER.info("Loan approved. Passed rules: {}", result.passedRules());
+ } else {
+ LOGGER.info("Loan rejected. Failed rules: {}", result.failedRules());
+ }
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/CreditScoreRule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/CreditScoreRule.java
new file mode 100644
index 000000000000..28cd59baadbb
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/CreditScoreRule.java
@@ -0,0 +1,61 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import lombok.extern.slf4j.Slf4j;
+
+/** Passes when the applicant's credit score reaches the required minimum. */
+@Slf4j
+public class CreditScoreRule implements Rule {
+
+ private final int minimumScore;
+
+ /**
+ * Creates the rule.
+ *
+ * @param minimumScore the inclusive minimum credit score the applicant must have
+ */
+ public CreditScoreRule(int minimumScore) {
+ this.minimumScore = minimumScore;
+ }
+
+ @Override
+ public String name() {
+ return "CreditScoreRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.creditScore() >= minimumScore;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info(
+ "Applicant credit score {} meets the minimum score of {}.",
+ context.creditScore(),
+ minimumScore);
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/LoanApplication.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/LoanApplication.java
new file mode 100644
index 000000000000..0eda1f17d3e2
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/LoanApplication.java
@@ -0,0 +1,38 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+/**
+ * Immutable context that the loan {@link Rule rules} are evaluated against.
+ *
+ * Using a record keeps the domain object immutable and free of behaviour, so the business logic
+ * lives entirely inside the rules rather than being scattered across the data it operates on.
+ *
+ * @param age applicant age in years
+ * @param monthlyIncome applicant net monthly income
+ * @param loanAmount requested loan amount
+ * @param creditScore applicant credit score
+ */
+public record LoanApplication(int age, double monthlyIncome, double loanAmount, int creditScore) {}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumAgeRule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumAgeRule.java
new file mode 100644
index 000000000000..71532dde4707
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumAgeRule.java
@@ -0,0 +1,58 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import lombok.extern.slf4j.Slf4j;
+
+/** Passes when the applicant is at least the required minimum age. */
+@Slf4j
+public class MinimumAgeRule implements Rule {
+
+ private final int minimumAge;
+
+ /**
+ * Creates the rule.
+ *
+ * @param minimumAge the inclusive minimum age the applicant must reach
+ */
+ public MinimumAgeRule(int minimumAge) {
+ this.minimumAge = minimumAge;
+ }
+
+ @Override
+ public String name() {
+ return "MinimumAgeRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.age() >= minimumAge;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info("Applicant age {} meets the minimum age of {}.", context.age(), minimumAge);
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumIncomeRule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumIncomeRule.java
new file mode 100644
index 000000000000..fc6d40964fc9
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/MinimumIncomeRule.java
@@ -0,0 +1,61 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import lombok.extern.slf4j.Slf4j;
+
+/** Passes when the applicant's monthly income reaches the required minimum. */
+@Slf4j
+public class MinimumIncomeRule implements Rule {
+
+ private final double minimumIncome;
+
+ /**
+ * Creates the rule.
+ *
+ * @param minimumIncome the inclusive minimum monthly income the applicant must earn
+ */
+ public MinimumIncomeRule(double minimumIncome) {
+ this.minimumIncome = minimumIncome;
+ }
+
+ @Override
+ public String name() {
+ return "MinimumIncomeRule";
+ }
+
+ @Override
+ public boolean evaluate(LoanApplication context) {
+ return context.monthlyIncome() >= minimumIncome;
+ }
+
+ @Override
+ public void execute(LoanApplication context) {
+ LOGGER.info(
+ "Applicant income {} meets the minimum income of {}.",
+ context.monthlyIncome(),
+ minimumIncome);
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/Rule.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/Rule.java
new file mode 100644
index 000000000000..723b31e73b52
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/Rule.java
@@ -0,0 +1,61 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+/**
+ * Abstraction for a single, self-contained business rule.
+ *
+ * A rule separates the decision ({@link #evaluate(Object)}) from the action ({@link
+ * #execute(Object)}). {@code evaluate} inspects the context and reports whether the rule's
+ * condition is satisfied, while {@code execute} performs the side effect associated with a
+ * satisfied rule. Keeping the two responsibilities apart lets a {@link RuleEngine} decide which
+ * rules apply before running any action, and lets new rules be added without touching the engine.
+ *
+ * @param type of the context the rule is evaluated against
+ */
+public interface Rule {
+
+ /**
+ * Returns a human readable identifier used when reporting rule outcomes.
+ *
+ * @return the rule name
+ */
+ String name();
+
+ /**
+ * Evaluates the rule against the given context.
+ *
+ * @param context the immutable input the rule inspects
+ * @return {@code true} when the rule's condition is satisfied, {@code false} otherwise
+ */
+ boolean evaluate(T context);
+
+ /**
+ * Performs the action associated with a satisfied rule.
+ *
+ * @param context the immutable input the action operates on
+ */
+ void execute(T context);
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngine.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngine.java
new file mode 100644
index 000000000000..85a8db9d74b5
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngine.java
@@ -0,0 +1,91 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+/**
+ * Orchestrates a fixed collection of {@link Rule rules} against a context.
+ *
+ * The engine owns the coordination logic that would otherwise live in a large nested {@code
+ * if}/{@code else} block: it evaluates every rule in insertion order, runs the action of each rule
+ * whose condition is satisfied, and reports the combined outcome as a {@link RuleEngineResult}.
+ * Evaluation never stops early, so a single run reports all failing rules rather than only
+ * the first, which is what makes rejection reasons useful.
+ *
+ *
The rule collection is copied defensively on construction, making the engine immutable and
+ * safe to reuse across many contexts. New behaviour is added by supplying additional {@link Rule}
+ * implementations; the engine itself never changes.
+ *
+ * @param type of the context the rules are evaluated against
+ */
+public class RuleEngine {
+
+ private final List> rules;
+
+ /**
+ * Creates an engine for the given rules.
+ *
+ * @param rules the rules to evaluate, applied in iteration order; must not be {@code null} or
+ * contain {@code null} elements
+ * @throws NullPointerException if {@code rules} is {@code null} or contains a {@code null} rule
+ */
+ public RuleEngine(List> rules) {
+ this.rules = List.copyOf(rules);
+ }
+
+ /**
+ * Evaluates every rule against the context and executes the rules that pass.
+ *
+ * @param context the context inspected by the rules; must not be {@code null}
+ * @return the combined outcome of the run
+ * @throws NullPointerException if {@code context} is {@code null}
+ */
+ public RuleEngineResult run(T context) {
+ Objects.requireNonNull(context, "context must not be null");
+ List passed = new ArrayList<>();
+ List failed = new ArrayList<>();
+ for (Rule rule : rules) {
+ if (rule.evaluate(context)) {
+ rule.execute(context);
+ passed.add(rule.name());
+ } else {
+ failed.add(rule.name());
+ }
+ }
+ return new RuleEngineResult(failed.isEmpty(), passed, failed);
+ }
+
+ /**
+ * Returns the rules held by this engine.
+ *
+ * @return an immutable view of the configured rules
+ */
+ public List> rules() {
+ return rules;
+ }
+}
diff --git a/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngineResult.java b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngineResult.java
new file mode 100644
index 000000000000..868e3986602f
--- /dev/null
+++ b/rule-engine/src/main/java/com/iluwatar/ruleengine/RuleEngineResult.java
@@ -0,0 +1,48 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import java.util.List;
+
+/**
+ * Immutable outcome produced by {@link RuleEngine#run(Object)}.
+ *
+ * The result reports whether every rule passed and, for transparency, the names of the rules
+ * that passed and failed in the order they were evaluated. The lists are copied on construction so
+ * the result cannot be mutated by callers or by later changes to the source collections.
+ *
+ * @param approved {@code true} when no rule failed
+ * @param passedRules names of the rules whose condition was satisfied, in evaluation order
+ * @param failedRules names of the rules whose condition was not satisfied, in evaluation order
+ */
+public record RuleEngineResult(
+ boolean approved, List passedRules, List failedRules) {
+
+ /** Defensive copy keeps the value object immutable. */
+ public RuleEngineResult {
+ passedRules = List.copyOf(passedRules);
+ failedRules = List.copyOf(failedRules);
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/AppTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/AppTest.java
new file mode 100644
index 000000000000..a8746fa0c387
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/AppTest.java
@@ -0,0 +1,37 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+
+import org.junit.jupiter.api.Test;
+
+class AppTest {
+
+ @Test
+ void shouldLaunchApp() {
+ assertDoesNotThrow(() -> App.main(new String[] {}));
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/LoanRulesTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/LoanRulesTest.java
new file mode 100644
index 000000000000..80ba160b1ffa
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/LoanRulesTest.java
@@ -0,0 +1,81 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+/** Verifies each concrete rule independently at, above and below its threshold. */
+class LoanRulesTest {
+
+ private static LoanApplication applicant(int age, double income, int score) {
+ return new LoanApplication(age, income, 10000.0, score);
+ }
+
+ @Test
+ void minimumAgeRulePassesAtOrAboveThreshold() {
+ var rule = new MinimumAgeRule(18);
+ assertTrue(rule.evaluate(applicant(18, 3000.0, 700)));
+ assertTrue(rule.evaluate(applicant(40, 3000.0, 700)));
+ assertDoesNotThrow(() -> rule.execute(applicant(18, 3000.0, 700)));
+ }
+
+ @Test
+ void minimumAgeRuleFailsBelowThreshold() {
+ var rule = new MinimumAgeRule(18);
+ assertFalse(rule.evaluate(applicant(17, 3000.0, 700)));
+ }
+
+ @Test
+ void minimumIncomeRulePassesAtOrAboveThreshold() {
+ var rule = new MinimumIncomeRule(2000.0);
+ assertTrue(rule.evaluate(applicant(30, 2000.0, 700)));
+ assertTrue(rule.evaluate(applicant(30, 5000.0, 700)));
+ assertDoesNotThrow(() -> rule.execute(applicant(30, 2000.0, 700)));
+ }
+
+ @Test
+ void minimumIncomeRuleFailsBelowThreshold() {
+ var rule = new MinimumIncomeRule(2000.0);
+ assertFalse(rule.evaluate(applicant(30, 1999.99, 700)));
+ }
+
+ @Test
+ void creditScoreRulePassesAtOrAboveThreshold() {
+ var rule = new CreditScoreRule(650);
+ assertTrue(rule.evaluate(applicant(30, 3000.0, 650)));
+ assertTrue(rule.evaluate(applicant(30, 3000.0, 800)));
+ assertDoesNotThrow(() -> rule.execute(applicant(30, 3000.0, 650)));
+ }
+
+ @Test
+ void creditScoreRuleFailsBelowThreshold() {
+ var rule = new CreditScoreRule(650);
+ assertFalse(rule.evaluate(applicant(30, 3000.0, 649)));
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineResultTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineResultTest.java
new file mode 100644
index 000000000000..0bae5dc12c0d
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineResultTest.java
@@ -0,0 +1,55 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class RuleEngineResultTest {
+
+ @Test
+ void copiesListsDefensivelyOnConstruction() {
+ var passed = new ArrayList<>(List.of("MinimumAgeRule"));
+ var failed = new ArrayList<>(List.of("CreditScoreRule"));
+ var result = new RuleEngineResult(false, passed, failed);
+
+ passed.add("MinimumIncomeRule"); // mutate the sources after construction
+ failed.clear();
+
+ assertEquals(List.of("MinimumAgeRule"), result.passedRules());
+ assertEquals(List.of("CreditScoreRule"), result.failedRules());
+ }
+
+ @Test
+ void exposedListsAreUnmodifiable() {
+ var result = new RuleEngineResult(true, List.of("MinimumAgeRule"), List.of());
+ assertThrows(UnsupportedOperationException.class, () -> result.passedRules().add("x"));
+ assertThrows(UnsupportedOperationException.class, () -> result.failedRules().add("x"));
+ }
+}
diff --git a/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineTest.java b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineTest.java
new file mode 100644
index 000000000000..fd8f89943ac9
--- /dev/null
+++ b/rule-engine/src/test/java/com/iluwatar/ruleengine/RuleEngineTest.java
@@ -0,0 +1,122 @@
+/*
+ * This project is licensed under the MIT license. Module model-view-viewmodel is using ZK framework licensed under LGPL (see lgpl-3.0.txt).
+ *
+ * The MIT License
+ * Copyright © 2014-2022 Ilkka Seppälä
+ *
+ * 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 com.iluwatar.ruleengine;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+
+class RuleEngineTest {
+
+ private static RuleEngine loanEngine() {
+ return new RuleEngine<>(
+ List.of(new MinimumAgeRule(18), new MinimumIncomeRule(2000.0), new CreditScoreRule(650)));
+ }
+
+ @Test
+ void approvesWhenEveryRulePasses() {
+ var result = loanEngine().run(new LoanApplication(30, 3000.0, 10000.0, 700));
+ assertTrue(result.approved());
+ assertEquals(
+ List.of("MinimumAgeRule", "MinimumIncomeRule", "CreditScoreRule"), result.passedRules());
+ assertTrue(result.failedRules().isEmpty());
+ }
+
+ @Test
+ void rejectsWhenASingleRuleFails() {
+ var result = loanEngine().run(new LoanApplication(30, 3000.0, 10000.0, 600));
+ assertFalse(result.approved());
+ assertEquals(List.of("CreditScoreRule"), result.failedRules());
+ assertEquals(List.of("MinimumAgeRule", "MinimumIncomeRule"), result.passedRules());
+ }
+
+ @Test
+ void reportsEveryFailingRuleWithoutStoppingEarly() {
+ var result = loanEngine().run(new LoanApplication(16, 500.0, 10000.0, 400));
+ assertFalse(result.approved());
+ assertEquals(
+ List.of("MinimumAgeRule", "MinimumIncomeRule", "CreditScoreRule"), result.failedRules());
+ assertTrue(result.passedRules().isEmpty());
+ }
+
+ @Test
+ void evaluatesRulesInInsertionOrder() {
+ var engine =
+ new RuleEngine<>(
+ List.of(
+ new CreditScoreRule(650), new MinimumAgeRule(18), new MinimumIncomeRule(2000.0)));
+ var result = engine.run(new LoanApplication(16, 500.0, 10000.0, 400));
+ assertEquals(
+ List.of("CreditScoreRule", "MinimumAgeRule", "MinimumIncomeRule"), result.failedRules());
+ }
+
+ @Test
+ void emptyEngineApprovesVacuously() {
+ var result =
+ new RuleEngine(List.of()).run(new LoanApplication(1, 1.0, 1.0, 1));
+ assertTrue(result.approved());
+ assertTrue(result.passedRules().isEmpty());
+ assertTrue(result.failedRules().isEmpty());
+ }
+
+ @Test
+ void rejectsNullContext() {
+ assertThrows(NullPointerException.class, () -> loanEngine().run(null));
+ }
+
+ @Test
+ void rejectsNullRuleCollection() {
+ assertThrows(NullPointerException.class, () -> new RuleEngine(null));
+ }
+
+ @Test
+ void rejectsNullRuleElement() {
+ var rules = new ArrayList>();
+ rules.add(null);
+ assertThrows(NullPointerException.class, () -> new RuleEngine<>(rules));
+ }
+
+ @Test
+ void copiesRulesDefensivelyOnConstruction() {
+ var rules = new ArrayList>();
+ rules.add(new MinimumAgeRule(18));
+ var engine = new RuleEngine<>(rules);
+
+ rules.add(new CreditScoreRule(650)); // mutate the source after construction
+ assertEquals(1, engine.rules().size());
+ }
+
+ @Test
+ void exposedRulesAreUnmodifiable() {
+ var engine = loanEngine();
+ assertThrows(
+ UnsupportedOperationException.class, () -> engine.rules().add(new MinimumAgeRule(21)));
+ }
+}