Skip to content

Commit

Permalink
Merge pull request #22 from ToxicBakery/feature/syncrhonized-machine
Browse files Browse the repository at this point in the history
Added a synchronized machine
  • Loading branch information
ToxicBakery authored Jul 14, 2018
2 parents 6e1dc1c + 1dc324e commit 8dbc23d
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 19 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ before_script:
stages:
- name: compile
- name: deploy
if: tag IS present OR branch = master
if: tag IS present OR (branch = master AND type != pull_request)

jobs:
include:
Expand Down
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
buildscript {
ext.kotlin_version = '1.2.41'
ext.kotlin_version = '1.2.51'
ext.hyperion_verion = '0.9.23'
ext.supportLibrary_version = '27.1.1'
ext.rxjava_version = '2.1.9'
Expand Down
27 changes: 12 additions & 15 deletions core/src/main/java/com/toxicbakery/kfinstatemachine/StateMachine.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,35 +6,32 @@ open class StateMachine<S> : IStateMachine<S> {

private val transitionRules: Array<out TransitionRule<S, *>>

final override var state: S

constructor(initialState: S, vararg transitionRules: TransitionRule<S, *>) {
_state = initialState
state = initialState
this.transitionRules = transitionRules
}

constructor(initialState: S, transitions: List<TransitionRule<S, *>>) {
_state = initialState
state = initialState
transitionRules = transitions.toTypedArray()
}

private var _state: S

override val state: S
get() = _state

override val transitions: Set<KClass<*>>
get() = transitionRules.filter { it.oldState == _state }
get() = transitionRules.filter { it.oldState == state }
.map { it.transition }
.toSet()

override fun transition(transition: Any): Unit =
edge(transition)
.also { _state = it.newState }
.also { state = it.newState }
.performReactions(this, transition)

override fun transitionsTo(targetState: S): Set<KClass<*>> =
transitionRules
.filter { rule: TransitionRule<S, *> ->
rule.oldState == _state
rule.oldState == state
&& rule.newState == targetState
}
.map(TransitionRule<S, *>::transition)
Expand All @@ -43,16 +40,16 @@ open class StateMachine<S> : IStateMachine<S> {
@Suppress("UNCHECKED_CAST")
private fun edge(transition: Any): TransitionRule<S, *> = transitionRules
.filter { transitionRule ->
transitionRule.oldState == _state
transitionRule.oldState == state
&& transitionRule.transition.java.isInstance(transition)
&& transitionRule.validate(transition)
}
.let { transitions: List<TransitionRule<S, *>> ->
when {
transitions.isEmpty() ->
throw Exception("Invalid transition `${transition.javaClass.simpleName}` for state `$_state`.\nValid transitions ${this.transitions}")
throw Exception("Invalid transition `${transition.javaClass.simpleName}` for state `$state`.\nValid transitions ${this.transitions}")
transitions.size > 1 ->
throw Exception("Ambiguous transition `${transition.javaClass.simpleName}` for state `$_state`.\nMatches ${transitions.toTransitionsString()}.")
throw Exception("Ambiguous transition `${transition.javaClass.simpleName}` for state `$state`.\nMatches ${transitions.toTransitionsString()}.")
else -> transitions.first()
}
}
Expand Down Expand Up @@ -88,9 +85,9 @@ data class TransitionRule<S, T : Any>(

@Suppress("UNCHECKED_CAST")
internal fun validate(transition: Any) = validations
.fold(true, { acc: Boolean, validation: (transition: T) -> Boolean ->
.fold(true) { acc: Boolean, validation: (transition: T) -> Boolean ->
acc && validation(transition as T)
})
}

@Suppress("UNCHECKED_CAST")
internal fun performReactions(machine: StateMachine<S>, transition: Any) = reactions
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.toxicbakery.kfinstatemachine

import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.reflect.KClass

class SynchronizedStateMachine<F>(
private val stateMachine: IStateMachine<F>,
private val lock: Lock = ReentrantLock(true)
) : IStateMachine<F> {

override val state: F
get() = lock.withLock { stateMachine.state }

override val transitions: Set<KClass<*>>
get() = lock.withLock { stateMachine.transitions }

override fun transition(transition: Any) =
lock.withLock { stateMachine.transition(transition) }

override fun transitionsTo(targetState: F): Set<KClass<*>> =
lock.withLock { stateMachine.transitionsTo(targetState) }

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
package com.toxicbakery.kfinstatemachine

import com.toxicbakery.kfinstatemachine.StateMachine.Companion.transition
import com.toxicbakery.kfinstatemachine.SynchronizedStateMachineTest.Login.*
import org.junit.Assert.*
import org.junit.Test
import kotlin.reflect.KClass

class SynchronizedStateMachineTest {

enum class Login {
PROMPT,
AUTHORIZING,
AUTHORIZED
}

sealed class HttpCode {
object Ok : HttpCode()
object NotAuthorized : HttpCode()
}

data class Credentials(
val username: String,
val password: String)

@Test
fun performTransition() {
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic),
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)))

assertEquals(Energy.Potential, stateMachine.state)

stateMachine.transition(EnergyTransition.Release)
assertEquals(Energy.Kinetic, stateMachine.state)

stateMachine.transition(EnergyTransition.Store)
assertEquals(Energy.Potential, stateMachine.state)
}

@Test
fun performTransition_altConstructor() {
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
listOf(
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic),
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)
)))

assertEquals(Energy.Potential, stateMachine.state)

stateMachine.transition(EnergyTransition.Release)
assertEquals(Energy.Kinetic, stateMachine.state)

stateMachine.transition(EnergyTransition.Store)
assertEquals(Energy.Potential, stateMachine.state)
}

@Test
fun performTransition_withActions() {
var externalStateTracking: Energy = Energy.Potential
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic)
.reaction { _, _ -> externalStateTracking = Energy.Kinetic },
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)
.reaction { _, _ -> externalStateTracking = Energy.Potential }))

assertEquals(Energy.Potential, stateMachine.state)
assertEquals(Energy.Potential, externalStateTracking)

stateMachine.transition(EnergyTransition.Release)
assertEquals(Energy.Kinetic, stateMachine.state)
assertEquals(Energy.Kinetic, externalStateTracking)

stateMachine.transition(EnergyTransition.Store)
assertEquals(Energy.Potential, stateMachine.state)
assertEquals(Energy.Potential, externalStateTracking)
}

@Test
fun performTransition_withRules() {
class LoginMachine {

private val stateMachine: IStateMachine<Login>
private val _steps: MutableList<Login> = mutableListOf(PROMPT)

val steps: List<Login>
get() = _steps

init {
stateMachine = SynchronizedStateMachine(StateMachine(
PROMPT,
transition(PROMPT, Credentials::class, AUTHORIZING)
.reaction { _, credentials ->
_steps.add(AUTHORIZING)
doLogin(credentials)
},
transition(AUTHORIZING, HttpCode::class, AUTHORIZED)
.onlyIf { it === HttpCode.Ok }
.reaction { _, _ -> _steps.add(AUTHORIZED) },
transition(AUTHORIZING, HttpCode::class, PROMPT)
.onlyIf { it === HttpCode.NotAuthorized }
.reaction { _, _ -> _steps.add(Login.PROMPT) }))
}

fun login(credentials: Credentials) = stateMachine.transition(credentials)

private fun doLogin(credentials: Credentials) =
when (credentials) {
Credentials("user", "correct password") -> HttpCode.Ok
else -> HttpCode.NotAuthorized
}.let(stateMachine::transition)

}

val loginMachine = LoginMachine()
loginMachine.login(Credentials("user", "incorrect password"))
loginMachine.login(Credentials("user", "correct password"))

assertEquals(
listOf(
PROMPT,
AUTHORIZING,
PROMPT,
AUTHORIZING,
AUTHORIZED),
loginMachine.steps)
}

@Test
fun performTransition_invalidTransition() {
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic),
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)))

try {
stateMachine.transition(EnergyTransition.Store)
fail("Exception expected")
} catch (e: Exception) {
assertTrue(e.message?.startsWith("Invalid transition ") ?: false)
}
}

@Test
fun performTransition_ambiguousTransition() {
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic),
transition<Energy, EnergyTransition.Release>(Energy.Potential, EnergyTransition.Release::class, Energy.Potential),
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)))

try {
stateMachine.transition(EnergyTransition.Release)
fail("Exception expected")
} catch (e: Exception) {
assertTrue(e.message?.startsWith("Ambiguous transition ") ?: false)
}
}

@Test
fun availableTransitions() {
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic),
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)))

assertEquals(
setOf(EnergyTransition.Release::class),
stateMachine.transitions)

stateMachine.transition(EnergyTransition.Release)

assertEquals(
setOf(EnergyTransition.Store::class),
stateMachine.transitions)
}

@Test
fun transitionsForTargetState() {
val stateMachine = SynchronizedStateMachine(StateMachine(
Energy.Potential,
transition(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic),
transition(Energy.Kinetic, EnergyTransition.Store::class, Energy.Potential)))

assertEquals(
setOf(EnergyTransition.Release::class),
stateMachine.transitionsTo(Energy.Kinetic))

assertEquals(
setOf<KClass<*>>(),
stateMachine.transitionsTo(Energy.Potential))

stateMachine.transition(EnergyTransition.Release)

assertEquals(
setOf(EnergyTransition.Store::class),
stateMachine.transitionsTo(Energy.Potential))

assertEquals(
setOf<KClass<*>>(),
stateMachine.transitionsTo(Energy.Kinetic))
}

@Test
fun validateRulesRules() {
TransitionRule(Energy.Potential, EnergyTransition.Release::class, Energy.Kinetic)
.onlyIf { transition -> transition === EnergyTransition.Release }
.onlyIf { transition -> transition === EnergyTransition.Release }
.validate(EnergyTransition.Release)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ open class RxStateMachine<F>(
private val stateMachine: IStateMachine<F>
) : IStateMachine<F> {

private val subject: Subject<F> by lazy { BehaviorSubject.createDefault(state) }
private val subject: Subject<F> = BehaviorSubject.createDefault(state)

val observable: Observable<F> = subject

override val state: F
final override val state: F
get() = stateMachine.state

override val transitions: Set<KClass<*>>
Expand Down

0 comments on commit 8dbc23d

Please sign in to comment.