Skip to content

Commit

Permalink
Use builders instead of data classes
Browse files Browse the repository at this point in the history
Some of public api data classes are stable (it is not problem) but some of them are extended frequantly.
Extended data classes are problem for binary compatibility. Every change is a breaking change.
So this commit changes extendable public data classes to builders.
  • Loading branch information
nsk90 committed Sep 4, 2024
1 parent 1122324 commit a634ae8
Show file tree
Hide file tree
Showing 24 changed files with 282 additions and 193 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ fun createStateMachineBlocking(
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
creationArguments: CreationArguments = buildCreationArguments {},
init: suspend BuildingStateMachine.() -> Unit
) = with(CoroutinesLibCoroutineAbstraction(scope)) {
runBlocking {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ suspend fun createStateMachine(
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
creationArguments: CreationArguments = buildCreationArguments {},
init: suspend BuildingStateMachine.() -> Unit
) = CoroutinesLibCoroutineAbstraction(scope)
.createStateMachine(name, childMode, start, creationArguments, init)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import ru.nsk.kstatemachine.state.IState
import ru.nsk.kstatemachine.state.InternalState
import ru.nsk.kstatemachine.statemachine.StateMachine

annotation class VisibleForTesting

/**
* [forEach] analog which ignores internal [StateMachine]s
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,7 @@
package ru.nsk.kstatemachine.coroutines

import ru.nsk.kstatemachine.state.ChildMode
import ru.nsk.kstatemachine.statemachine.BuildingStateMachine
import ru.nsk.kstatemachine.statemachine.StateMachine
import ru.nsk.kstatemachine.statemachine.*
import ru.nsk.kstatemachine.statemachine.StateMachineImpl
import kotlin.coroutines.Continuation
import kotlin.coroutines.EmptyCoroutineContext
Expand Down Expand Up @@ -57,7 +56,7 @@ suspend fun CoroutineAbstraction.createStateMachine(
name: String?,
childMode: ChildMode,
start: Boolean,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
creationArguments: CreationArguments = buildCreationArguments {},
init: suspend BuildingStateMachine.() -> Unit
): StateMachine = StateMachineImpl(
name,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,31 +20,45 @@ interface MetaInfo
/**
* Standard [MetaInfo], to control export PlantUML and Mermaid feature visualization.
*/
interface IUmlMetaInfo : MetaInfo {
interface UmlMetaInfo : MetaInfo {
/**
* Will be mapped to "long name" for [IState], and a "label" for [Transition]
* Default: null
*/
val umlLabel: String?

/**
* Add description lines for [IState]
* Does not have effect for [Transition]
* Default: emptyList()
*/
val umlStateDescriptions: List<String>

/**
* For [IState] translated to "note right of".
* For [Transition] translated to "note on link" (supports only one note).
* Mermaid does not support this, so it will not take any effect.
* Default: emptyList()
*/
val umlNotes: List<String>
}

/**
* [IUmlMetaInfo] Implementation is separated from its interface as a user may combine multiple [MetaInfo]
* interfaces into one object.
* [UmlMetaInfo] Implementation is separated from its interface as a user may combine multiple [MetaInfo]
* interfaces into one object. Data class should not be exposed to public APIs due to binary compatibility, users should
* use [buildUmlMetaInfo] instead.
*/
data class UmlMetaInfo(
override val umlLabel: String? = null,
override val umlStateDescriptions: List<String> = emptyList(),
override val umlNotes: List<String> = emptyList(),
): IUmlMetaInfo
interface UmlMetaInfoBuilder : UmlMetaInfo {
override var umlLabel: String?
override var umlStateDescriptions: List<String>
override var umlNotes: List<String>
}

private data class UmlMetaInfoBuilderImpl(
override var umlLabel: String? = null,
override var umlStateDescriptions: List<String> = emptyList(),
override var umlNotes: List<String> = emptyList(),
) : UmlMetaInfoBuilder

fun buildUmlMetaInfo(builder: UmlMetaInfoBuilder.() -> Unit): UmlMetaInfo =
UmlMetaInfoBuilderImpl().apply(builder).copy()
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@

package ru.nsk.kstatemachine.persistence

import ru.nsk.kstatemachine.VisibleForTesting
import ru.nsk.kstatemachine.event.DestroyEvent
import ru.nsk.kstatemachine.event.StopEvent
import ru.nsk.kstatemachine.statemachine.*
import ru.nsk.kstatemachine.statemachine.StateMachine.EventRecordingArguments
import ru.nsk.kstatemachine.transition.EventAndArgument
import ru.nsk.kstatemachine.visitors.structureHashCode

Expand All @@ -23,17 +23,54 @@ interface EventRecorder {
}

/**
* This object is intended to be serialized by client code
* This class is intended to be serialized by client code
*/
data class RecordedEvents(
class RecordedEvents @VisibleForTesting constructor(
val structureHashCode: Int,
val records: List<Record>,
)
) {

data class Record(
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as RecordedEvents

if (structureHashCode != other.structureHashCode) return false
if (records != other.records) return false

return true
}

override fun hashCode(): Int {
var result = structureHashCode
result = 31 * result + records.hashCode()
return result
}
}

class Record @VisibleForTesting constructor(
val eventAndArgument: EventAndArgument<*>,
val processingResult: ProcessingResult,
)
) {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Record

if (eventAndArgument != other.eventAndArgument) return false
if (processingResult != other.processingResult) return false

return true
}

override fun hashCode(): Int {
var result = eventAndArgument.hashCode()
result = 31 * result + processingResult.hashCode()
return result
}
}

internal class EventRecorderImpl(
private val machine: StateMachine,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,12 @@ import ru.nsk.kstatemachine.event.StartEvent
import ru.nsk.kstatemachine.statemachine.*
import ru.nsk.kstatemachine.visitors.structureHashCode

data class RestorationResult(
class RestorationResult internal constructor(
val results: List<RestoredEventResult>,
val warnings: List<RestorationWarningException>,
)

data class RestoredEventResult(
class RestoredEventResult internal constructor(
val record: Record,
val processingResult: Result<ProcessingResult>,
val warnings: List<RestorationWarningException>,
Expand All @@ -27,7 +27,7 @@ enum class WarningType {
RecordedAndProcessedEventCountNotMatch,
}

class RestorationWarningException(
class RestorationWarningException internal constructor(
val warningType: WarningType,
message: String,
cause: Throwable? = null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Author: Mikhail Fedotov
* Github: https://github.com/KStateMachine
* Copyright (c) 2024.
* All rights reserved.
*/

package ru.nsk.kstatemachine.statemachine

interface CreationArguments {
/**
* Allows the library to automatically call destroy() on current state owning machine instance if user tries
* to reuse its states in another machine. Usually this is a result of using object states in sequentially created
* similar machines. destroy() will be called on the previous machine instance.
* If set to false an exception will be thrown on state reuse attempt.
* Default: true
*/
val autoDestroyOnStatesReuse: Boolean

/**
* Enables Undo transition.
* Default: false
*/
val isUndoEnabled: Boolean

/**
* If set to true, when multiple transitions match event the first matching transition is selected.
* if set to false, when multiple transitions match event exception is thrown.
* Default: false
*/
val doNotThrowOnMultipleTransitionsMatch: Boolean

/**
* If enabled, throws exception on the machine start,
* if it contains states or transitions with null or blank names
* Default: false
*/
val requireNonBlankNames: Boolean

/**
* If set, enables incoming events recording in order to restore [StateMachine] later.
* By default, event recording is disabled.
* Use [StateMachine.eventRecorder] to access the recording result.
* Default: null
*/
val eventRecordingArguments: EventRecordingArguments?
}

interface CreationArgumentsBuilder : CreationArguments {
override var autoDestroyOnStatesReuse: Boolean
override var isUndoEnabled: Boolean
override var doNotThrowOnMultipleTransitionsMatch: Boolean
override var requireNonBlankNames: Boolean
override var eventRecordingArguments: EventRecordingArguments?
}

private data class CreationArgumentsBuilderImpl(
override var autoDestroyOnStatesReuse: Boolean = true,
override var isUndoEnabled: Boolean = false,
override var doNotThrowOnMultipleTransitionsMatch: Boolean = false,
override var requireNonBlankNames: Boolean = false,
override var eventRecordingArguments: EventRecordingArguments? = null
) : CreationArgumentsBuilder

fun buildCreationArguments(builder: CreationArgumentsBuilder.() -> Unit): CreationArguments =
CreationArgumentsBuilderImpl().apply(builder).copy()

interface EventRecordingArguments {
/**
* If enabled removes all recorded events when detects that the machine was stopped and started again.
* Default: true
*/
val clearRecordsOnMachineRestart: Boolean

/**
* If enabled skips ignored events, supposing they do not affect restoration of the machine
* Default: true
*/
val skipIgnoredEvents: Boolean
}

interface EventRecordingArgumentsBuilder : EventRecordingArguments {
override var clearRecordsOnMachineRestart: Boolean
override var skipIgnoredEvents: Boolean
}

private data class EventRecordingArgumentsBuilderImpl(
override var clearRecordsOnMachineRestart: Boolean = true,
override var skipIgnoredEvents: Boolean = true,
) : EventRecordingArgumentsBuilder

fun buildEventRecordingArguments(builder: EventRecordingArgumentsBuilder.() -> Unit): EventRecordingArguments =
EventRecordingArgumentsBuilderImpl().apply(builder).copy()
Original file line number Diff line number Diff line change
Expand Up @@ -158,48 +158,6 @@ interface StateMachine : State {
*/
suspend fun onException(exception: Exception)
}

data class CreationArguments(
/**
* Allows the library to automatically call destroy() on current state owning machine instance if user tries
* to reuse its states in another machine. Usually this is a result of using object states in sequentially created
* similar machines. destroy() will be called on the previous machine instance.
* If set to false an exception will be thrown on state reuse attempt.
*/
val autoDestroyOnStatesReuse: Boolean = true,
/**
* Enables Undo transition
*/
val isUndoEnabled: Boolean = false,
/**
* If set to true, when multiple transitions match event the first matching transition is selected.
* if set to false, when multiple transitions match event exception is thrown.
* Default if false.
*/
val doNotThrowOnMultipleTransitionsMatch: Boolean = false,
/**
* If enabled, throws exception on the machine start,
* if it contains states or transitions with null or blank names
*/
val requireNonBlankNames: Boolean = false,
/**
* If set, enables incoming events recording in order to restore [StateMachine] later.
* By default, event recording is disabled.
* Use [StateMachine.eventRecorder] to access the recording result.
*/
val eventRecordingArguments: EventRecordingArguments? = null
)

data class EventRecordingArguments(
/**
* If enabled removes all recorded events when detects that the machine was stopped and started again.
*/
val clearRecordsOnMachineRestart: Boolean = true,
/**
* If enabled skips ignored events, supposing they do not affect restoration of the machine
*/
val skipIgnoredEvents: Boolean = true,
)
}

fun StateMachine.startBlocking(argument: Any? = null) = coroutineAbstraction.runBlocking { start(argument) }
Expand Down Expand Up @@ -288,7 +246,7 @@ fun createStdLibStateMachine(
name: String? = null,
childMode: ChildMode = ChildMode.EXCLUSIVE,
start: Boolean = true,
creationArguments: StateMachine.CreationArguments = StateMachine.CreationArguments(),
creationArguments: CreationArguments = buildCreationArguments {},
init: suspend BuildingStateMachine.() -> Unit
): StateMachine {
return with(StdLibCoroutineAbstraction()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import ru.nsk.kstatemachine.persistence.EventRecorderImpl
import ru.nsk.kstatemachine.state.*
import ru.nsk.kstatemachine.state.pseudo.UndoState
import ru.nsk.kstatemachine.statemachine.ProcessingResult.*
import ru.nsk.kstatemachine.statemachine.StateMachine.CreationArguments
import ru.nsk.kstatemachine.transition.*
import ru.nsk.kstatemachine.transition.TransitionDirectionProducerPolicy.DefaultPolicy
import ru.nsk.kstatemachine.visitors.CheckUniqueNamesVisitor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import ru.nsk.kstatemachine.event.WrappedEvent
import ru.nsk.kstatemachine.statemachine.StateMachineDslMarker

@StateMachineDslMarker
data class TransitionParams<E : Event>(
data class TransitionParams<E : Event> internal constructor(
val transition: Transition<E>,
val direction: TransitionDirection,
val event: E,
Expand Down
Loading

0 comments on commit a634ae8

Please sign in to comment.