Skip to content

Commit

Permalink
Store rule config in db (#6)
Browse files Browse the repository at this point in the history
* Implement database storage for rule configurations

- Remove commented-out code in `RuleEngineApplication` to clean up the class.
- Introduce `RuleConfigController` to handle HTTP requests for rule configurations.
- Define JPA entities: `RuleSetDefinition`, `RuleDefinition`, `ConditionDefinition`, and `ActionDefinition` for database mapping.
- Create repositories for each entity to facilitate database operations.
- Implement `RuleService` to manage rule configurations using the repositories.
- Add `YamlRuleConfigLoader` to load rules from YAML configuration.
- Update `build.gradle.kts` to include Spring Data JPA and MySQL dependencies.
- Configure Spring Boot application properties for MySQL database connection.
- Add SQL scripts to create necessary tables and constraints for rule configurations.

* Refactor rule configuration to be stored in the database

- Adjust data classes in RuleConfig to include default values for fields.
- Add SQL script to create the `rule_set_definition_entity` table and insert initial data.
- Remove un-used classes

* Refactor rule configuration to be stored in the database

- Add SQL script to create the `rule_set_definition_entity` table and insert initial

* Store RuleSetDefinition in DB

- Config JPA with noArg
- Implement DbRuleConfigService
- Implement RuleConfigController
- Remove odd classes

* Refactor RuleConfiguration to use Spring ConfigurationProperties

* Completed save RuleConfig to DB
  • Loading branch information
thanhtrixx authored Jan 12, 2025
1 parent 839014d commit 9da9327
Show file tree
Hide file tree
Showing 15 changed files with 315 additions and 147 deletions.
15 changes: 14 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ plugins {
kotlin("plugin.spring") version "1.9.25"
id("org.springframework.boot") version "3.4.1"
id("io.spring.dependency-management") version "1.1.7"
kotlin("plugin.jpa") version "1.8.22" // Apply the JPA plugin
kotlin("kapt") version "1.8.22"
id("org.jetbrains.kotlin.plugin.noarg") version "1.9.25" // Apply the no-arg plugin
}

group = "trile"
Expand Down Expand Up @@ -35,7 +38,10 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-log4j2")
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.5.0")

runtimeOnly("com.h2database:h2")
implementation("org.springframework.boot:spring-boot-starter-data-jpa")

runtimeOnly("com.mysql:mysql-connector-j")

runtimeOnly("org.springframework.modulith:spring-modulith-actuator")
runtimeOnly("org.springframework.modulith:spring-modulith-observability")
testImplementation("org.springframework.boot:spring-boot-starter-test")
Expand All @@ -44,6 +50,7 @@ dependencies {
testImplementation("org.springframework.modulith:spring-modulith-starter-test")
testImplementation("org.testcontainers:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testRuntimeOnly("com.h2database:h2")
testImplementation("io.mockk:mockk:1.13.7")
}

Expand All @@ -53,6 +60,12 @@ dependencyManagement {
}
}

noArg {
annotation("jakarta.persistence.Entity") // Add the @Entity annotation
annotation("jakarta.persistence.MappedSuperclass")
annotation("jakarta.persistence.Embeddable")
}

kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
Expand Down
25 changes: 3 additions & 22 deletions src/main/kotlin/trile/RuleEngineApplication.kt
Original file line number Diff line number Diff line change
@@ -1,37 +1,18 @@
package trile

import java.math.BigDecimal
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.runApplication
import trile.common.Log
import trile.rule.engine.RuleEngine
import trile.rule.model.RuleConfiguration
import trile.rule.model.TransactionContext
import trile.rule.model.RuleSetDefinition

@SpringBootApplication
class RuleEngineApplication(
private val ruleConfig: RuleConfiguration,
private val ruleEngine: RuleEngine
private val ruleSetsByUseCase: Map<String, RuleSetDefinition>,
) : 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 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)
l.info("Loaded rule config: [$ruleSetsByUseCase]")
}
}

Expand Down
34 changes: 34 additions & 0 deletions src/main/kotlin/trile/rule/config/RuleConfigController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package trile.rule.config

import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
import trile.rule.model.RuleSetDefinition

@RestController
@RequestMapping("/api/rules")
class RuleConfigController(
private val ruleConfigService: RuleConfigService,
private val ruleSetsByUseCase: Map<String, RuleSetDefinition>
) {

@GetMapping
fun getAllRules(): ResponseEntity<List<RuleSetDefinition>> {
return ResponseEntity.ok(ruleConfigService.getAllRuleSets())
}

@GetMapping("rule-set-by-use-case")
fun getRuleSetByUserCase() = ruleSetsByUseCase

// @PostMapping
// fun createRuleSet(@RequestBody ruleSet: RuleSetDefinition): ResponseEntity<RuleSetDefinition> {
// return ResponseEntity.ok(ruleService.saveRuleSet(ruleSet))
// }
//
// @DeleteMapping("/{id}")
// fun deleteRuleSet(@PathVariable id: Long): ResponseEntity<Void> {
// ruleService.deleteRuleSet(id)
// return ResponseEntity.noContent().build()
// }
}
11 changes: 11 additions & 0 deletions src/main/kotlin/trile/rule/config/RuleConfigService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package trile.rule.config

import trile.rule.model.RuleSetDefinition


interface RuleConfigService {
fun getAllRuleSets(): List<RuleSetDefinition>
fun createRuleSet(ruleSet: RuleSetDefinition): RuleSetDefinition
fun updateRuleSet(id: Long, ruleSet: RuleSetDefinition): RuleSetDefinition?
fun deleteRuleSet(id: Long)
}
33 changes: 33 additions & 0 deletions src/main/kotlin/trile/rule/config/RuleConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package trile.rule.config

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import trile.rule.model.RuleSetDefinition

@Configuration
data class RuleConfiguration(
private val ruleProperties: RuleProperties,
private val ruleConfigService: RuleConfigService
) {

@Bean
fun ruleSetsByUseCase(): Map<String, RuleSetDefinition> {
return when (ruleProperties.source) {
RuleConfigurationSource.YAML -> ruleProperties.ruleSets
RuleConfigurationSource.DB -> ruleConfigService.getAllRuleSets()
}.associateBy { it.useCase }
}
}

@Configuration
@ConfigurationProperties(prefix = "rules")
data class RuleProperties(
var source: RuleConfigurationSource = RuleConfigurationSource.YAML,
var ruleSets: List<RuleSetDefinition> = emptyList()
)

enum class RuleConfigurationSource {
DB,
YAML
}
25 changes: 25 additions & 0 deletions src/main/kotlin/trile/rule/config/Runner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//package trile.rule.config
//
//import jakarta.transaction.Transactional
//import org.springframework.boot.CommandLineRunner
//import org.springframework.stereotype.Component
//import trile.common.Log
//import trile.rule.config.db.RuleSetDefinitionRepository
//import trile.rule.model.RuleSetDefinition
//
//@Component
//class RuleDataInitializer(
// private val ruleConfigService: RuleConfigService,
// private val ruleProperties: RuleProperties,
// private val ruleSetDefinitionRepository: RuleSetDefinitionRepository,
// private val ruleSetsByUseCase: Map<String, RuleSetDefinition>,
//) : CommandLineRunner, Log {
//
// @Transactional
// override fun run(vararg args: String?) {
// l.info("Loaded rule config: [$ruleSetsByUseCase]")
//
// ruleSetDefinitionRepository.findAll().forEach { ruleSetDefinitionRepository.deleteById(it.id) }
// ruleProperties.ruleSets.forEach { ruleConfigService.createRuleSet(it) }
// }
//}
41 changes: 41 additions & 0 deletions src/main/kotlin/trile/rule/config/db/DbRuleConfigService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package trile.rule.config.db

import org.springframework.stereotype.Service
import trile.common.Log
import trile.rule.config.RuleConfigService
import trile.rule.model.RuleSetDefinition

@Service
//@ConditionalOnProperty(prefix = "rules", name = ["source"], havingValue = "DB")
class DbRuleConfigService(
private val ruleSetRepository: RuleSetDefinitionRepository,
) : RuleConfigService, Log {


override fun getAllRuleSets(): List<RuleSetDefinition> {
return ruleSetRepository.findAll().map { entity ->
RuleSetDefinition(useCase = entity.useCase, name = entity.name, rules = entity.rules)
}
}


override fun createRuleSet(ruleSet: RuleSetDefinition): RuleSetDefinition {
val entity = RuleSetDefinitionEntity(useCase = ruleSet.useCase, name = ruleSet.name, rules = ruleSet.rules)
val savedEntity = ruleSetRepository.save(entity)
return RuleSetDefinition(useCase = ruleSet.useCase, name = savedEntity.name, rules = savedEntity.rules)
}

override fun updateRuleSet(id: Long, ruleSet: RuleSetDefinition): RuleSetDefinition {
return ruleSetRepository.findById(id).map { existingEntity ->
existingEntity.useCase = ruleSet.useCase
existingEntity.name = ruleSet.name
existingEntity.rules = ruleSet.rules
val updatedEntity = ruleSetRepository.save(existingEntity)
RuleSetDefinition(useCase = updatedEntity.useCase, name = updatedEntity.name, rules = updatedEntity.rules)
}.orElseThrow()
}

override fun deleteRuleSet(id: Long) {
ruleSetRepository.deleteById(id)
}
}
27 changes: 27 additions & 0 deletions src/main/kotlin/trile/rule/config/db/RuleConfigEntities.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package trile.rule.config.db


import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import org.hibernate.annotations.JdbcTypeCode
import org.hibernate.type.SqlTypes
import trile.rule.model.RuleDefinition

@Entity
data class RuleSetDefinitionEntity(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long = 0,

@Column(name = "use-case")
var useCase: String,

var name: String,

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "rules", columnDefinition = "json")
var rules: List<RuleDefinition>
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package trile.rule.config.db

import org.springframework.data.jpa.repository.JpaRepository

interface RuleSetDefinitionRepository : JpaRepository<RuleSetDefinitionEntity, Long>
6 changes: 3 additions & 3 deletions src/main/kotlin/trile/rule/engine/RuleEngine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,19 @@ 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
import trile.rule.model.RuleDefinition
import trile.rule.model.RuleSetDefinition
import trile.rule.model.TransactionContext

@Service
class RuleEngine(
ruleConfig: RuleConfiguration,
ruleSetsByUseCase: Map<String, RuleSetDefinition>,
private val conditionMap: Map<ConditionType, Condition<Any>>,
private val actionMap: Map<ActionType, Action<Any>>
) : Log {

private val ruleExecutorsByType: Map<String, RuleExecutor> =
ruleConfig.useCases
ruleSetsByUseCase
.map {
it.key to RuleExecutor(it.value.name, createRules(it.value.rules))
}.toMap()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,31 +1,25 @@
package trile.rule.model

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.context.annotation.Configuration
import trile.rule.action.ActionType
import trile.rule.condition.ConditionType
import trile.rule.model.Constants.Companion.EMPTY_CONDITIONS
import trile.rule.model.Constants.Companion.EMPTY_MAP

@Configuration
@ConfigurationProperties(prefix = "rules")
data class RuleConfiguration(
val useCases: Map<String, RuleSetDefinition>
)
import trile.rule.model.Constants.Companion.EMPTY_STRING

data class RuleSetDefinition(
val name: String,
val useCase: String = EMPTY_STRING,
val name: String = EMPTY_STRING,
val rules: List<RuleDefinition>
)

data class RuleDefinition(
val name: String?,
val name: String = EMPTY_STRING,
val conditions: List<ConditionDefinition> = emptyList(),
val actions: List<ActionDefinition>
)

data class ConditionDefinition(
val name: String?,
val name: String = EMPTY_STRING,
val type: ConditionType,
val parameters: Map<String, String> = EMPTY_MAP,
val or: List<ConditionDefinition> = EMPTY_CONDITIONS,
Expand All @@ -34,7 +28,7 @@ data class ConditionDefinition(
)

data class ActionDefinition(
val name: String?,
val name: String = EMPTY_STRING,
val type: ActionType,
val parameters: Map<String, String> = EMPTY_MAP
)
Expand Down
Loading

0 comments on commit 9da9327

Please sign in to comment.