Skip to content

Commit

Permalink
Operator condition (#1)
Browse files Browse the repository at this point in the history
* Add ConditionType.OPERATOR

* Implement OperatorCondition
Refactor Condition and RuleEngine
Move logic create conditionMap & actionMap to RuleContext

* Update Class diagram
  • Loading branch information
thanhtrixx authored Dec 29, 2024
1 parent 21ad916 commit 1c23b56
Show file tree
Hide file tree
Showing 12 changed files with 154 additions and 43 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120
max_line_length = 160
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ classDiagram
+ executeRules(context: TransactionContext)
}
class RuleContext {
+ createConditionMap(context: TransactionContext): - Map~String, Condition~
+ createActionMap(context: TransactionContext): - Map~String, Action~
}
class RuleExecutor {
- String name
- List~RuleSetWrapper~ rules
Expand Down Expand Up @@ -59,19 +64,18 @@ classDiagram
class Condition {
+ evaluate(context: TransactionContext, parameter: ParameterType): Boolean
+ convertParameter(parameters: Map~String, Any~): ParameterType
}
class Action {
+ execute(context: TransactionContext, parameter: ParameterType)
+ convertParameter(parameters: Map~String, Any~): ParameterType
}
RuleEngine --> RuleExecutor : manages
RuleExecutor --> RuleSetWrapper : contains
RuleSetWrapper --> ConditionWithParameter : has
RuleSetWrapper --> ActionWithParameter : has
RuleEngine --> RuleConfiguration : uses
RuleContext --> RuleEngine : inject
RuleConfiguration --> RuleSetDefinition : uses
RuleSetDefinition --> RuleDefinition : uses
ConditionWithParameter --> Condition : references
Expand Down
20 changes: 13 additions & 7 deletions src/main/kotlin/trile/RuleEngineApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,24 @@ import trile.rule.model.TransactionContext
class RuleEngineApplication(
private val ruleConfig: RuleConfiguration,
private val ruleEngine: RuleEngine
) : ApplicationRunner, Log {
) : ApplicationRunner, Log() {
override fun run(args: ApplicationArguments?) {
l.info("Loaded rule config: [$ruleConfig]")


val context = TransactionContext(type = "regular-card", amount = BigDecimal.valueOf(10000))
l.info("Executing with context: [$context]")
ruleEngine.executeRules(context)
// val context = TransactionContext(type = "regular-card", amount = BigDecimal.valueOf(10000))
// l.info("Executing with context: [$context]")
// ruleEngine.executeRules(context)
//
// val notRunContext = TransactionContext(type = "regular-card", amount = BigDecimal.valueOf(10))
// l.info("Executing with context: [$notRunContext]")
// ruleEngine.executeRules(notRunContext)

val notRunContext = TransactionContext(type = "regular-card", amount = BigDecimal.valueOf(10))
l.info("Executing with context: [$notRunContext]")
ruleEngine.executeRules(notRunContext)
val superCardRunContext = TransactionContext(type = "super-card", amount = BigDecimal.valueOf(10))
ruleEngine.executeRules(superCardRunContext)

val superCardNotRunContext = TransactionContext(type = "super-card", amount = BigDecimal.valueOf(10000))
ruleEngine.executeRules(superCardNotRunContext)
}
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/trile/common/Log.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package trile.common

import org.apache.logging.log4j.kotlin.cachedLoggerOf

interface Log {
val l
abstract class Log {
protected val l
get() = cachedLoggerOf(this.javaClass)
}
2 changes: 1 addition & 1 deletion src/main/kotlin/trile/rule/action/EarnPointByRateAction.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import trile.common.getAndToBigDecimal
import trile.rule.model.TransactionContext

@Component
class EarnPointByRateAction : Action<BigDecimal>, Log {
class EarnPointByRateAction : Action<BigDecimal>, Log() {
override fun execute(context: TransactionContext, parameter: BigDecimal) {
l.info("Earn-point with amount [${context.amount}], rate [$parameter], point [${context.amount.multiply(parameter)}]")
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/trile/rule/condition/Condition.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ interface Condition<out T> {

fun evaluate(context: TransactionContext, parameter: @UnsafeVariance T): Boolean
val type: ConditionType
fun convertParameter(parameters: Map<String, String>): T
}

enum class ConditionType {
OPERATOR,
EQUALS,
GREATER_THAN,
LESS_THAN,
IN_RANGE,
IN_CATEGORIES,
CONTAINS
CONTAINS,
}
3 changes: 0 additions & 3 deletions src/main/kotlin/trile/rule/condition/EqualsCondition.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package trile.rule.condition

import java.math.BigDecimal
import org.springframework.stereotype.Component
import trile.common.getAndToBigDecimal
import trile.rule.model.TransactionContext

@Component
Expand All @@ -12,6 +11,4 @@ class EqualsCondition : Condition<BigDecimal> {
}

override val type = ConditionType.EQUALS

override fun convertParameter(parameters: Map<String, String>) = parameters.getAndToBigDecimal("amount")
}
35 changes: 35 additions & 0 deletions src/main/kotlin/trile/rule/condition/OperatorCondition.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package trile.rule.condition

import org.springframework.core.Ordered
import org.springframework.core.annotation.Order
import org.springframework.stereotype.Component
import trile.rule.engine.ConditionWithParameter
import trile.rule.model.TransactionContext

@Order(value = Ordered.LOWEST_PRECEDENCE - 1000)
@Component
class OperatorCondition : Condition<OperatorConditionParameter> {

override fun evaluate(
context: TransactionContext, parameter: OperatorConditionParameter
): Boolean {
return when (parameter.type) {
OperatorType.NOT -> !parameter.conditions.all { it.condition.evaluate(context, it.parameter) }
OperatorType.AND -> parameter.conditions.all { it.condition.evaluate(context, it.parameter) }
OperatorType.OR -> parameter.conditions.any { it.condition.evaluate(context, it.parameter) }
}
}

override val type: ConditionType = ConditionType.OPERATOR
}

data class OperatorConditionParameter(
val type: OperatorType,
val conditions: List<ConditionWithParameter<Any>>
)

enum class OperatorType {
NOT,
AND,
OR
}
73 changes: 57 additions & 16 deletions src/main/kotlin/trile/rule/engine/RuleEngine.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
package trile.rule.engine

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.stereotype.Service
import trile.common.Log
import trile.common.getAndToBigDecimal
import trile.common.getOrThrow
import trile.rule.action.Action
import trile.rule.action.ActionType
import trile.rule.condition.Condition
import trile.rule.condition.ConditionType
import trile.rule.condition.OperatorConditionParameter
import trile.rule.condition.OperatorType
import trile.rule.model.ActionDefinition
import trile.rule.model.ConditionDefinition
import trile.rule.model.RuleConfiguration
Expand All @@ -14,14 +21,9 @@ import trile.rule.model.TransactionContext
@Service
class RuleEngine(
ruleConfig: RuleConfiguration,
conditions: List<Condition<Any>>,
actions: List<Action<Any>>
) : Log {

private val conditionMap =
conditions.associateBy { it.type }.also { l.info("Loaded conditions [${it.keys.joinToString()}]") }

private val actionMap = actions.associateBy { it.type }.also { l.info("Loaded actions [${it.keys.joinToString()}]") }
private val conditionMap: Map<ConditionType, Condition<Any>>,
private val actionMap: Map<ActionType, Action<Any>>
) : Log() {

private val executorByType: Map<String, RuleExecutor> =
ruleConfig.paymentUseCases
Expand All @@ -30,6 +32,7 @@ class RuleEngine(
}.toMap()

fun executeRules(context: TransactionContext) {
l.info("Executing with context [$context]")
val executor = executorByType[context.type]
if (executor == null) {
l.info("Cannot get executor for type ${context.type}. Ignored this transaction")
Expand All @@ -38,39 +41,77 @@ class RuleEngine(
executor.execute(context)
}

private fun ConditionDefinition.toConditionWithParameter(): ConditionWithParameter<Any> {
val condition = conditionMap.getOrThrow(this.type, "Can not find condition for type [$type]")
return ConditionWithParameter(condition = condition, parameter = condition.convertParameter(this))
private fun ConditionDefinition.toConditionWithParameter(conditionMap: Map<ConditionType, Condition<Any>>): ConditionWithParameter<Any> {
val condition =
conditionMap.getOrThrow(this.type, "Can not find condition for type [$type]")
return ConditionWithParameter(
condition = condition,
parameter = convertParameter(conditionMap)
)
}

private fun ConditionDefinition.convertParameter(
conditionMap: Map<ConditionType, Condition<Any>>
): Any {
return when (this.type) {
ConditionType.EQUALS -> parameters.getAndToBigDecimal("amount")
ConditionType.OPERATOR -> {
val (type, conditions) = when {
this.not.isNotEmpty() -> OperatorType.NOT to this.not
this.and.isNotEmpty() -> OperatorType.AND to this.and
this.or.isNotEmpty() -> OperatorType.OR to this.or

else -> throw RuntimeException("Missing sub condition")
}
return OperatorConditionParameter(
type = type,
conditions = conditions.map { it.toConditionWithParameter(conditionMap) })
}

else -> RuntimeException("Not supported")
}
}

private fun ActionDefinition.toActionWithParameter(): ActionWithParameter<Any> {
private fun ActionDefinition.toActionWithParameter(actionMap: Map<ActionType, Action<Any>>): ActionWithParameter<Any> {
val action = actionMap.getOrThrow(type, "Can not find action for type: [${type}]")
return ActionWithParameter(action = action, parameter = action.convertParameter(parameters))
}


private fun createRules(ruleSetConfigurationDefinitions: List<RuleDefinition>): List<RuleSetWrapper<Any, Any>> {
return ruleSetConfigurationDefinitions.map { rule ->
RuleSetWrapper(
conditions = rule.conditions.map { it.toConditionWithParameter() },
actions = rule.actions.map { it.toActionWithParameter() }
conditions = rule.conditions.map { it.toConditionWithParameter(conditionMap) },
actions = rule.actions.map { it.toActionWithParameter(actionMap) }
)
}
}

private class RuleExecutor(
private val name: String,
private val rules: List<RuleSetWrapper<Any, Any>>
) {
) : Log() {

fun execute(context: TransactionContext) {
for (rule in rules) {
if (rule.conditions.all { it.condition.evaluate(context, it.parameter) }) {
rule.actions.forEach { it.action.execute(context, it.parameter) }
}
}
l.info("Executed $name")
}
}
}

@Configuration
internal class RuleContext(
private val conditions: List<Condition<Any>>,
private val actions: List<Action<Any>>
) : Log() {

@Bean
fun conditionMap() =
conditions.associateBy { it.type }.also { l.info("Loaded conditions [${it.keys.joinToString()}]") }

@Bean
fun actionMap() = actions.associateBy { it.type }.also { l.info("Loaded actions [${it.keys.joinToString()}]") }
}
3 changes: 3 additions & 0 deletions src/main/kotlin/trile/rule/engine/RuleSetWrapper.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
package trile.rule.engine

import trile.common.getOrThrow
import trile.rule.action.Action
import trile.rule.condition.Condition
import trile.rule.condition.ConditionType
import trile.rule.model.ConditionDefinition

data class RuleSetWrapper<C, A>(
val conditions: List<ConditionWithParameter<C>>,
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/trile/rule/model/RuleConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ data class ConditionDefinition(
val parameters: Map<String, String> = EMPTY_MAP,
val or: List<ConditionDefinition> = EMPTY_CONDITIONS,
val and: List<ConditionDefinition> = EMPTY_CONDITIONS,
val not: ConditionDefinition?
val not: List<ConditionDefinition> = EMPTY_CONDITIONS,
)

data class ActionDefinition(
Expand Down
41 changes: 33 additions & 8 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,46 @@ spring:

rules:
payment-use-cases:
regular-card:
name: Regular Card Purchases
# regular-card:
# name: Regular Card Purchases
# rules:
# - name: Earn points for regular card purchases over 10k
# conditions:
# - type: EQUALS
# parameters:
# amount: 10000
# actions:
# - type: EARN_POINT_BY_RATE
# parameters:
# rate: 0.02 # Use decimal for percentage (2%)
# - type: EARN_POINT_BY_RATE
# parameters:
# rate: 0.05 # Use decimal for percentage (2%)
super-card:
name: Super Card Purchases
rules:
- name: Earn points for regular card purchases over 10k
conditions:
- type: EQUALS
parameters:
amount: 10000
- type: OPERATOR
name: Not equals 10000
not:
- name: Not equals 10000
type: EQUALS
parameters:
amount: 10000
# or:
# - type: EQUALS
# parameters:
# amount: 10000
# - type: EQUALS
# parameters:
# amount: 10001


actions:
- type: EARN_POINT_BY_RATE
parameters:
rate: 0.02 # Use decimal for percentage (2%)
- type: EARN_POINT_BY_RATE
parameters:
rate: 0.05 # Use decimal for percentage (2%)

#
# - name: Earn points for regular card purchases over 10k
Expand Down

0 comments on commit 1c23b56

Please sign in to comment.