Skip to content

BAEL-9162 - Simple Rule Engine #18742

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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions rule-engines-modules/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<module>evrete</module>
<module>openl-tablets</module>
<module>rulebook</module>
<module>simple-rule-engine</module>
<!-- <module>jess</module> --> <!-- requires dependencies which are not publicly available -->
</modules>

Expand Down
43 changes: 43 additions & 0 deletions rule-engines-modules/simple-rule-engine/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baeldung.simpleruleengine</groupId>
<artifactId>simple-rule-engine</artifactId>
<version>1.0</version>
<name>simple-rule-engine</name>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>9</source>
<target>9</target>
</configuration>
</plugin>
</plugins>
</build>

<parent>
<groupId>com.baeldung</groupId>
<artifactId>rule-engines-modules</artifactId>
<version>1.0.0-SNAPSHOT</version>
</parent>

<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>7.0.0-M7</version>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.baeldung.ruleengine;

import com.baeldung.ruleengine.model.Order;

public class FirstOrderHighValueSpecialDiscountRule implements IRule {

@Override
public boolean evaluate(Order order) {
return order.getCustomer()
.isFirstOrder() && order.getAmount() > 500;
}

@Override
public String description() {
return "First Order Special Discount Rule: First Time customer with high value order";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.baeldung.ruleengine;

import com.baeldung.ruleengine.model.Order;

public interface IRule {
boolean evaluate(Order order);
String description();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.baeldung.ruleengine;

import com.baeldung.ruleengine.model.Order;

public class LoyaltyDiscountRule implements IRule{

@Override
public boolean evaluate(Order order) {
return order.getCustomer().getLoyaltyPoints() > 500;
}

@Override
public String description() {
return "Loyalty Discount Rule: Customer has more than 500 points";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.baeldung.ruleengine;

import java.util.List;
import java.util.stream.Collectors;

import com.baeldung.ruleengine.model.Order;

public class RuleEngine {
private final List<IRule> rules;

public RuleEngine(List<IRule> rules) {
this.rules = rules;
}

public List<String> evaluate(Order order) {
return rules.stream()
.filter(rule -> rule.evaluate(order))
.map(IRule::description)
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.baeldung.ruleengine;

import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;

import com.baeldung.ruleengine.model.Order;


public class SpelRule {

private final String expression;
private final String description;

public SpelRule(String expression, String description) {
this.expression = expression;
this.description = description;
}

public boolean evaluate(Order order) {
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext(order);
context.setVariable("order", order);
return parser.parseExpression(expression)
.getValue(context, Boolean.class);
}

public String getDescription() {
return description;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.baeldung.ruleengine.model;

import lombok.AccessLevel;
import lombok.Getter;
import lombok.Setter;
import lombok.experimental.FieldDefaults;

@Setter
@Getter
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Customer {

String name;
int loyaltyPoints;
boolean firstOrder;

public Customer() {
}

public Customer(String name, int loyaltyPoints, boolean firstOrder) {
this.name = name;
this.loyaltyPoints = loyaltyPoints;
this.firstOrder = firstOrder;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.baeldung.ruleengine.model;

import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.experimental.FieldDefaults;

@Getter
@Setter
public class Order {

private Double amount;
private Customer customer;

public Order() {
}

public Order(Double amount, Customer customer) {
this.amount = amount;
this.customer = customer;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.baeldung.ruleengine;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;

import com.baeldung.ruleengine.model.Customer;
import com.baeldung.ruleengine.model.Order;

public class RuleEngineUnitTest {

@Test
void whenTwoRulesTriggered_thenBothDescriptionsReturned() {
Customer customer = new Customer("Max", 550, true);
Order order = new Order(600.0, customer);

RuleEngine engine = new RuleEngine(List.of(new LoyaltyDiscountRule(), new FirstOrderHighValueSpecialDiscountRule()));

List<String> results = engine.evaluate(order);

assertEquals(2, results.size());
assertTrue(results.contains("Loyalty Discount Rule: Customer has more than 500 points"));
assertTrue(results.contains("First Order Special Discount Rule: First Time customer with high value order"));
}

@Test
void whenNoRulesTriggered_thenEmptyListReturned() {
Customer customer = new Customer("Max", 50, false);
Order order = new Order(200.0, customer);

RuleEngine engine = new RuleEngine(List.of(new LoyaltyDiscountRule(), new FirstOrderHighValueSpecialDiscountRule()));

List<String> results = engine.evaluate(order);

assertTrue(results.isEmpty());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.baeldung.ruleengine;

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertTrue;

import com.baeldung.ruleengine.model.Customer;
import com.baeldung.ruleengine.model.Order;

public class SpelRuleUnitTest {

@Test
void whenLoyalCustomer_thenEligibleForDiscount() {
Customer customer = new Customer("Bob", 730, false);
Order order = new Order(200.0, customer);

SpelRule rule = new SpelRule(
"#order.customer.loyaltyPoints > 500",
"Loyalty discount rule"
);
assertTrue(rule.evaluate(order));
}

@Test
void whenFirstOrderHighAmount_thenEligibleForSpecialDiscount() {
Customer customer = new Customer("Bob", 0, true);
Order order = new Order(800.0, customer);

SpelRule approvalRule = new SpelRule(
"#order.customer.firstOrder and #order.amount > 500",
"First-time customer with high order gets special discount"
);
assertTrue(approvalRule.evaluate(order));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n
</pattern>
</encoder>
</appender>

<root level="INFO">
<appender-ref ref="STDOUT" />
</root>
</configuration>