Skip to content

Commit

Permalink
Introduce States argument to Transition (#37)
Browse files Browse the repository at this point in the history
* Introduce States argument to Transition

* Add test for States

* Update readme and changelog
  • Loading branch information
Synesso authored May 8, 2024
1 parent e5f529c commit c3aa322
Show file tree
Hide file tree
Showing 11 changed files with 97 additions and 21 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

## [Unreleased]

### Breaking

* Introduced States as a proxy for NonEmptySet when defining Transitions. This allows for safer transition definitions
in the non-Arrow library.
* The Arrow specific library will eventually be removed, as the non-Arrow presenting API has equivalent semantics.


## [0.4.0]

### Breaking
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,11 +59,11 @@ instead.

```kotlin
abstract class ColorChange(
from: NonEmptySet<Color>,
from: States<Color>,
to: Color
) : Transition<Light, Color>(from, to) {
// Convenience constructor for when the from set has only one value
constructor(from: Color, to: Color) : this(nonEmptySetOf(from), to)
constructor(from: Color, to: Color) : this(States(from), to)
}

class Go(private val camera: Camera) : ColorChange(from = Red, to = Green) {
Expand All @@ -86,7 +86,7 @@ persist values.
class LightTransitioner(
private val database: Database
) : Transitioner<ColorChange, Light, Color>(
persist = { it.also(database::update).right() }
persist = { Result.success(it.also(database::update)) }
)
```

Expand All @@ -98,11 +98,11 @@ It is sometimes necessary to execute effects before and after a transition. Thes

```kotlin
class LightTransitioner ... {
override suspend fun preHook(value: V, via: T): ErrorOr<Unit> = Either.catch {
override suspend fun preHook(value: V, via: T): Result<Unit> = runCatching {
globalLock.lock(value)
}

override suspend fun postHook(from: S, value: V, via: T): ErrorOr<Unit> = Either.catch {
override suspend fun postHook(from: S, value: V, via: T): Result<Unit> = runCatching {
globalLock.unlock(value)
notificationService.send(via.successNotifications())
}
Expand All @@ -116,7 +116,7 @@ transitioner.

```kotlin
val transitioner = LightTransitioner(database)
val greenLight: ErrorOr<Light> = transitioner.transition(redLight, Go)
val greenLight: Result<Light> = transitioner.transition(redLight, Go)
```

### More examples
Expand Down
24 changes: 22 additions & 2 deletions lib/api/lib.api
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,31 @@ public final class app/cash/kfsm/StateMachine {
public final fun verify-IoAF18A (Lapp/cash/kfsm/State;)Ljava/lang/Object;
}

public final class app/cash/kfsm/States {
public static final field Companion Lapp/cash/kfsm/States$Companion;
public fun <init> (Lapp/cash/kfsm/State;Ljava/util/Set;)V
public fun <init> (Lapp/cash/kfsm/State;[Lapp/cash/kfsm/State;)V
public final fun component1 ()Lapp/cash/kfsm/State;
public final fun component2 ()Ljava/util/Set;
public final fun copy (Lapp/cash/kfsm/State;Ljava/util/Set;)Lapp/cash/kfsm/States;
public static synthetic fun copy$default (Lapp/cash/kfsm/States;Lapp/cash/kfsm/State;Ljava/util/Set;ILjava/lang/Object;)Lapp/cash/kfsm/States;
public fun equals (Ljava/lang/Object;)Z
public final fun getA ()Lapp/cash/kfsm/State;
public final fun getOther ()Ljava/util/Set;
public final fun getSet ()Ljava/util/Set;
public fun hashCode ()I
public fun toString ()Ljava/lang/String;
}

public final class app/cash/kfsm/States$Companion {
public final fun toStates (Ljava/util/Set;)Lapp/cash/kfsm/States;
}

public class app/cash/kfsm/Transition {
public fun <init> (Lapp/cash/kfsm/State;Lapp/cash/kfsm/State;)V
public fun <init> (Ljava/util/Set;Lapp/cash/kfsm/State;)V
public fun <init> (Lapp/cash/kfsm/States;Lapp/cash/kfsm/State;)V
public fun effect-gIAlu-s (Lapp/cash/kfsm/Value;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
public final fun getFrom ()Ljava/util/Set;
public final fun getFrom ()Lapp/cash/kfsm/States;
public final fun getTo ()Lapp/cash/kfsm/State;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@ package app.cash.kfsm

class InvalidStateTransition(transition: Transition<*, *>, value: Value<*, *>) : Exception(
"Value cannot transition ${
transition.from.toList().sortedBy { it.toString() }.joinToString(", ", prefix = "{", postfix = "}")
transition.from.set.toList().sortedBy { it.toString() }.joinToString(", ", prefix = "{", postfix = "}")
} to ${transition.to}, because it is currently ${value.state}"
)
15 changes: 15 additions & 0 deletions lib/src/main/kotlin/app/cash/kfsm/States.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package app.cash.kfsm

/** A collection of states that is guaranteed to be non-empty. */
data class States<S : State>(val a: S, val other: Set<S>) {
constructor(first: S, vararg others: S) : this(first, others.toSet())

val set: Set<S> = other + a

companion object {
fun <S: State> Set<S>.toStates(): States<S> = when {
isEmpty() -> throw IllegalArgumentException("Cannot create States from empty set")
else -> toList().let { States(it.first(), it.drop(1).toSet()) }
}
}
}
7 changes: 3 additions & 4 deletions lib/src/main/kotlin/app/cash/kfsm/Transition.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
package app.cash.kfsm

open class Transition<V: Value<V, S>, S : State>(val from: Set<S>, val to: S) {
open class Transition<V: Value<V, S>, S : State>(val from: States<S>, val to: S) {

init {
require(from.isNotEmpty()) { "At least one from state must be defined" }
from.filterNot { it.canDirectlyTransitionTo(to) }.let {
from.set.filterNot { it.canDirectlyTransitionTo(to) }.let {
require(it.isEmpty()) { "invalid transition(s): ${it.map { from -> "$from->$to" }}" }
}
}

constructor(from: S, to: S) : this(setOf(from), to)
constructor(from: S, to: S) : this(States(from), to)

open suspend fun effect(value: V): Result<V> = Result.success(value)
}
2 changes: 1 addition & 1 deletion lib/src/main/kotlin/app/cash/kfsm/Transitioner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ abstract class Transitioner<T : Transition<V, S>, V : Value<V, S>, S : State>(
value: V,
transition: T
): Result<V> = when {
transition.from.contains(value.state) -> doTheTransition(value, transition)
transition.from.set.contains(value.state) -> doTheTransition(value, transition)
// Self-cycled transitions will be effected by the first case.
// If we still see a transition to self then this is a no-op.
transition.to == value.state -> ignoreAlreadyCompletedTransition(value, transition)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class InvalidStateTransitionTest : StringSpec({
}

"with many from-states has correct message" {
InvalidStateTransition(LetterTransition(setOf(C, B), D), Letter(E)).message shouldBe
InvalidStateTransition(LetterTransition(States(C, B), D), Letter(E)).message shouldBe
"Value cannot transition {B, C} to D, because it is currently E"
}
})
30 changes: 30 additions & 0 deletions lib/src/test/kotlin/app/cash/kfsm/StatesTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package app.cash.kfsm

import app.cash.kfsm.States.Companion.toStates
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.StringSpec
import io.kotest.matchers.shouldBe
import io.kotest.property.Arb
import io.kotest.property.arbitrary.element
import io.kotest.property.arbitrary.set
import io.kotest.property.checkAll

class StatesTest : StringSpec({

"vararg constructor" {
checkAll(Arb.set(arbChar, range = 1 .. 5)) { set ->
States(set.first(), set).set shouldBe set
set.toStates().set shouldBe set
}
}

"fails to create from empty set" {
shouldThrow<IllegalArgumentException> {
emptySet<Char>().toStates()
}
}
}) {
companion object {
val arbChar: Arb<Char> = Arb.element(A, B, C, D, E)
}
}
8 changes: 4 additions & 4 deletions lib/src/test/kotlin/app/cash/kfsm/TransitionTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,14 @@ class TransitionTest : StringSpec({
}

"cannot create an invalid state transition from a set of states" {
shouldThrow<IllegalArgumentException> { LetterTransition(setOf(B, A), C) }
shouldThrow<IllegalArgumentException> { LetterTransition(States(B, A), C) }
}

})

open class LetterTransition(from: Set<Char>, to: Char): Transition<Letter, Char>(from, to) {
constructor(from: Char, to: Char) : this(setOf(from), to)
open class LetterTransition(from: States<Char>, to: Char): Transition<Letter, Char>(from, to) {
constructor(from: Char, to: Char) : this(States(from), to)

val specificToThisTransitionType: String = "$from -> $to"
val specificToThisTransitionType: String = "${from.set} -> $to"
}

Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package app.cash.kfsm.exemplar

import app.cash.kfsm.States
import app.cash.kfsm.States.Companion.toStates
import app.cash.kfsm.Transition
import app.cash.kfsm.exemplar.Hamster.Asleep
import app.cash.kfsm.exemplar.Hamster.Awake
Expand All @@ -9,11 +11,14 @@ import app.cash.kfsm.exemplar.Hamster.RunningOnWheel

// Create your own base transition class in order to extend your transition collection with common functionality
abstract class HamsterTransition(
from: Set<Hamster.State>,
from: States<Hamster.State>,
to: Hamster.State
) : Transition<Hamster, Hamster.State>(from, to) {
// Convenience constructor for when the from set has only one value
constructor(from: Hamster.State, to: Hamster.State) : this(setOf(from), to)
constructor(from: Hamster.State, to: Hamster.State) : this(States(from), to)

// Convenience constructor for the deprecated variant that takes a set instead of States
constructor(from: Set<Hamster.State>, to: Hamster.State) : this(from.toStates(), to)

// Demonstrates how you can add base behaviour to transitions for use in pre and post hooks.
open val description: String = ""
Expand Down

0 comments on commit c3aa322

Please sign in to comment.