Skip to content
Merged
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
3 changes: 0 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ jobs:
- name: Run code quality checks (detekt)
run: ./gradlew detekt

- name: Run code formatting checks (kotlinter)
run: ./gradlew lintKotlinMain lintKotlinTest

- name: Build project
run: ./gradlew build -x test

Expand Down
58 changes: 16 additions & 42 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
// Kotlin version bumped to be compatible with the Maven Publish plugin and Gradle 8.10
kotlin("jvm") version "1.9.25"
id("io.gitlab.arturbosch.detekt") version "1.23.7"
id("org.jmailen.kotlinter") version "3.6.0"
kotlin("jvm") version "2.2.0"
id("io.gitlab.arturbosch.detekt") version "1.23.8"
id("maven-publish")
id("signing")
id("com.vanniktech.maven.publish") version "0.34.0"
id("net.researchgate.release") version "3.0.2"
id("net.researchgate.release") version "3.1.0"
jacoco
}

Expand All @@ -21,21 +20,18 @@ repositories {
}

dependencies {
// Logging dependencies are compileOnly (provided) to avoid transitive vulnerabilities
// Updated to 1.5.20 to fix CVE-2024-12798 (JaninoEventEvaluator vulnerability fixed in 1.5.13+)
// Available at runtime for local testing via testImplementation
compileOnly("io.github.microutils:kotlin-logging-jvm:3.0.5")
compileOnly("ch.qos.logback:logback-classic:1.5.20")
testImplementation("io.github.microutils:kotlin-logging-jvm:3.0.5")
testImplementation("ch.qos.logback:logback-classic:1.5.20")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.3.1")
testImplementation("io.mockk:mockk:1.13.11")
testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.11.0")
// SLF4J API only — consumers choose their own logging implementation
implementation("org.slf4j:slf4j-api:2.0.17")
compileOnly("ch.qos.logback:logback-classic:1.5.32")
testImplementation("ch.qos.logback:logback-classic:1.5.32")
testImplementation("org.mockito.kotlin:mockito-kotlin:5.4.0")
testImplementation("io.mockk:mockk:1.14.3")
testImplementation("org.junit.jupiter", "junit-jupiter-params", "5.12.2")
testImplementation(kotlin("test"))

// Jackson for JSON serialization
api("com.fasterxml.jackson.core:jackson-databind:2.17.2")
api("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2")
api("com.fasterxml.jackson.core:jackson-databind:2.19.0")
api("com.fasterxml.jackson.module:jackson-module-kotlin:2.19.0")
}

java {
Expand Down Expand Up @@ -98,32 +94,8 @@ signing {
}
}

kotlinter {
ignoreFailures = false
reporters = arrayOf("html")
experimentalRules = false
disabledRules = arrayOf(
"no-wildcard-imports",
"import-ordering",
"indent",
"final-newline",
"no-multi-spaces",
"no-trailing-spaces",
"string-template"
)
}

// Automatic formatting before checking
tasks.named("lintKotlinMain") {
dependsOn("formatKotlinMain")
}
tasks.named("lintKotlinTest") {
dependsOn("formatKotlinTest")
}

detekt {
config =
files("$projectDir/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior
config.setFrom("$projectDir/detekt.yml") // point to your custom config defining rules to run, overwriting default behavior
}

tasks.withType<io.gitlab.arturbosch.detekt.Detekt>().configureEach {
Expand Down Expand Up @@ -179,7 +151,9 @@ tasks.check {
}

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "11"
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_11)
}
}

// Kotlin DSL
Expand Down
50 changes: 29 additions & 21 deletions src/main/kotlin/io/github/ngirchev/fsm/impl/AbstractFsm.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package io.github.ngirchev.fsm.impl

import mu.KLogging
import io.github.ngirchev.fsm.*
import io.github.ngirchev.fsm.exception.FsmException
import io.github.ngirchev.fsm.exception.FsmTransitionFailedException
import org.slf4j.LoggerFactory
import java.util.concurrent.CopyOnWriteArrayList

/**
Expand All @@ -12,20 +12,24 @@ import java.util.concurrent.CopyOnWriteArrayList
abstract class AbstractFsm<STATE, TRANSITION : AbstractTransition<STATE>, TRANSITION_TABLE : AbstractTransitionTable<STATE, TRANSITION>>(
context: StateContext<STATE>,
open val transitionTable: TRANSITION_TABLE,
autoTransitionEnabled: Boolean? = null
) : StateSupport<STATE>, TransitionSupport<STATE, TRANSITION>, Notifiable<STATE> {

companion object : KLogging()
autoTransitionEnabled: Boolean? = null,
) : StateSupport<STATE>,
TransitionSupport<STATE, TRANSITION>,
Notifiable<STATE> {
companion object {
private val logger = LoggerFactory.getLogger(AbstractFsm::class.java)
}

/**
* Enable auto transitions based on transition table.
*/
val autoTransitionEnabled: Boolean

init {
val overrideAutoTransition = autoTransitionEnabled ?: run {
transitionTable.autoTransitionEnabled
}
val overrideAutoTransition =
autoTransitionEnabled ?: run {
transitionTable.autoTransitionEnabled
}
this.autoTransitionEnabled = overrideAutoTransition
}

Expand All @@ -36,14 +40,12 @@ abstract class AbstractFsm<STATE, TRANSITION : AbstractTransition<STATE>, TRANSI
) : this(
DefaultStateContext(state),
transitionTable,
autoTransitionEnabled
autoTransitionEnabled,
)

internal val context: StateContext<STATE> = context

override fun getState(): STATE {
return this.context.state
}
override fun getState(): STATE = this.context.state

private val stateChangeListeners = CopyOnWriteArrayList<StateChangeListener<STATE>>()

Expand All @@ -55,13 +57,17 @@ abstract class AbstractFsm<STATE, TRANSITION : AbstractTransition<STATE>, TRANSI
stateChangeListeners.remove(listener)
}

override fun notify(context: StateContext<STATE>, oldState: STATE, newState: STATE) {
logger.info { "Changed status $oldState -> $newState" }
override fun notify(
context: StateContext<STATE>,
oldState: STATE,
newState: STATE,
) {
logger.info("Changed status {} -> {}", oldState, newState)
stateChangeListeners.forEach { listener ->
try {
listener.onStateChanged(context, oldState, newState)
} catch (e: Exception) {
logger.error(e) { "Error in state change listener" }
logger.error("Error in state change listener", e)
}
}
}
Expand All @@ -82,10 +88,12 @@ abstract class AbstractFsm<STATE, TRANSITION : AbstractTransition<STATE>, TRANSI

private fun executeSingleTransition(transition: TRANSITION) {
val oldState = context.state
if (transition.from != oldState) throw FsmException(
"Current state $oldState doesn't fit " +
"to change, because transition from=[${transition.from}]"
)
if (transition.from != oldState) {
throw FsmException(
"Current state $oldState doesn't fit " +
"to change, because transition from=[${transition.from}]",
)
}
transition.to.timeout?.value?.also {
Thread.sleep(it * 1000)
}
Expand All @@ -104,7 +112,7 @@ abstract class AbstractFsm<STATE, TRANSITION : AbstractTransition<STATE>, TRANSI
val newState = transition.to.state

context.currentTransition = transition
logger.info { "Try to change status $oldState -> $newState" }
logger.info("Try to change status {} -> {}", oldState, newState)

transition.to.actions.forEach { it.invoke(context) }
context.state = newState
Expand All @@ -114,6 +122,6 @@ abstract class AbstractFsm<STATE, TRANSITION : AbstractTransition<STATE>, TRANSI

private class DefaultStateContext<STATE>(
override var state: STATE,
override var currentTransition: Transition<STATE>? = null
override var currentTransition: Transition<STATE>? = null,
) : StateContext<STATE>
}
22 changes: 11 additions & 11 deletions src/main/kotlin/io/github/ngirchev/fsm/impl/basic/BDomainFsm.kt
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
package io.github.ngirchev.fsm.impl.basic

import mu.KLogging
import io.github.ngirchev.fsm.StateContext
import io.github.ngirchev.fsm.impl.AbstractDomainFsm

open class BDomainFsm<DOMAIN : StateContext<STATE>, STATE>(
override val transitionTable: BTransitionTable<STATE>,
private val autoTransitionEnabled: Boolean? = null
private val autoTransitionEnabled: Boolean? = null,
) : AbstractDomainFsm<DOMAIN, STATE, BTransition<STATE>, BTransitionTable<STATE>>(
transitionTable
) {

companion object : KLogging()

override fun changeState(domain: DOMAIN, newState: STATE) {
val overrideAutoTransition = autoTransitionEnabled ?: run {
transitionTable.autoTransitionEnabled
}
transitionTable,
) {
override fun changeState(
domain: DOMAIN,
newState: STATE,
) {
val overrideAutoTransition =
autoTransitionEnabled ?: run {
transitionTable.autoTransitionEnabled
}
BFsm(domain, transitionTable, overrideAutoTransition).toState(newState)
}
}
41 changes: 26 additions & 15 deletions src/main/kotlin/io/github/ngirchev/fsm/impl/extended/ExDomainFsm.kt
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package io.github.ngirchev.fsm.impl.extended

import mu.KLogging
import io.github.ngirchev.fsm.impl.AbstractDomainFsm
import io.github.ngirchev.fsm.StateContext
import io.github.ngirchev.fsm.StateChangeListener
import io.github.ngirchev.fsm.StateContext
import io.github.ngirchev.fsm.impl.AbstractDomainFsm
import java.util.concurrent.CopyOnWriteArrayList

open class ExDomainFsm<DOMAIN : StateContext<STATE>, STATE, EVENT>(
override val transitionTable: ExTransitionTable<STATE, EVENT>,
private val autoTransitionEnabled: Boolean? = null
) : AbstractDomainFsm<DOMAIN, STATE, ExTransition<STATE, EVENT>, ExTransitionTable<STATE, EVENT>>(transitionTable), StateChangeListener<STATE> {

companion object : KLogging()

private val autoTransitionEnabled: Boolean? = null,
) : AbstractDomainFsm<DOMAIN, STATE, ExTransition<STATE, EVENT>, ExTransitionTable<STATE, EVENT>>(transitionTable),
StateChangeListener<STATE> {
private val stateChangeListeners = CopyOnWriteArrayList<StateChangeListener<STATE>>()

fun addStateChangeListener(listener: StateChangeListener<STATE>) {
Expand All @@ -30,24 +27,34 @@ open class ExDomainFsm<DOMAIN : StateContext<STATE>, STATE, EVENT>(
* The returned instance does not have any listeners attached.
*/
fun getFsmForDomain(domain: DOMAIN): ExFsm<STATE, EVENT> {
val overrideAutoTransition = autoTransitionEnabled ?: run {
transitionTable.autoTransitionEnabled
}
val overrideAutoTransition =
autoTransitionEnabled ?: run {
transitionTable.autoTransitionEnabled
}
return ExFsm(domain, transitionTable, overrideAutoTransition)
}

/**
* handle event for passed document.
*/
fun handle(domain: DOMAIN, event: EVENT) {
fun handle(
domain: DOMAIN,
event: EVENT,
) {
handleWithListeners(domain) { fsm -> fsm.onEvent(event) }
}

override fun changeState(domain: DOMAIN, newState: STATE) {
override fun changeState(
domain: DOMAIN,
newState: STATE,
) {
handleWithListeners(domain) { fsm -> fsm.toState(newState) }
}

fun handleWithListeners(domain: DOMAIN, action: (ExFsm<STATE, EVENT>) -> Unit) {
fun handleWithListeners(
domain: DOMAIN,
action: (ExFsm<STATE, EVENT>) -> Unit,
) {
val fsm = getFsmForDomain(domain)
fsm.addStateChangeListener(this)
try {
Expand All @@ -57,7 +64,11 @@ open class ExDomainFsm<DOMAIN : StateContext<STATE>, STATE, EVENT>(
}
}

override fun onStateChanged(context: StateContext<STATE>, oldState: STATE, newState: STATE) {
override fun onStateChanged(
context: StateContext<STATE>,
oldState: STATE,
newState: STATE,
) {
stateChangeListeners.forEach { listener ->
listener.onStateChanged(context, oldState, newState)
}
Expand Down
14 changes: 8 additions & 6 deletions src/test/kotlin/io/github/ngirchev/fsm/it/BFsmIT.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package io.github.ngirchev.fsm.it

import mu.KLogging
import org.slf4j.LoggerFactory
import org.junit.jupiter.api.Assertions
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
Expand All @@ -20,7 +20,9 @@ import kotlin.test.assertFailsWith

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class BFsmIT {
companion object : KLogging()
companion object {
private val logger = LoggerFactory.getLogger(BFsmIT::class.java)
}

private var autoSendEnabled: Boolean = true
private var successfullySent: Boolean = false
Expand All @@ -36,7 +38,7 @@ internal class BFsmIT {
To(
"SIGNED",
condition = { true },
action = { logger.info { "SIGNED SUCCESSFUL" } }),
action = { logger.info("SIGNED SUCCESSFUL") }),
To("CANCELLED")
)
.add(
Expand All @@ -45,7 +47,7 @@ internal class BFsmIT {
To(
state = "AUTO_SENT",
condition = { autoSendEnabled },
action = { successfullySent = true; logger.info { "AUTO SENT ACTION" } }
action = { successfullySent = true; logger.info("AUTO SENT ACTION") }
)
))
.add(from = "SIGNED", "DONE", "CANCELED")
Expand All @@ -56,11 +58,11 @@ internal class BFsmIT {
.autoTransitionEnabled(false)
.from("NEW").to("READY_FOR_SIGN").end()
.from("READY_FOR_SIGN").toMultiple()
.to("SIGNED").condition { true }.action { logger.info { "SIGNED SUCCESSFUL" } }.end()
.to("SIGNED").condition { true }.action { logger.info("SIGNED SUCCESSFUL") }.end()
.to("CANCELED").end().endMultiple()
.from("SIGNED").to("AUTO_SENT")
.condition { autoSendEnabled }
.action { successfullySent = true; logger.info { "AUTO SENT ACTION" } }.end()
.action { successfullySent = true; logger.info("AUTO SENT ACTION") }.end()
.from("SIGNED").toMultiple().to("DONE").end().to("CANCELED").end().endMultiple()
.from("AUTO_SENT").toMultiple().to("DONE").end().to("CANCELED").end().endMultiple()
.build()
Expand Down
Loading
Loading