Skip to content

Commit

Permalink
Merge branch 'resolution-hints'
Browse files Browse the repository at this point in the history
  • Loading branch information
nsk90 committed Dec 12, 2024
2 parents 91b6ce4 + 27fe906 commit b5ab6e9
Show file tree
Hide file tree
Showing 16 changed files with 816 additions and 170 deletions.
50 changes: 48 additions & 2 deletions docs/pages/export.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ title: Export
---

# Export

{: .no_toc }

## Page contents

{: .no_toc .text-delta }

- TOC
{:toc}
{:toc}

The library supports export into PlantUML and Mermaid diagram drawing systems. They both use PlantUML text format.
Mermaid supports fewer features then PlantUML itself.
Expand All @@ -27,6 +29,50 @@ but may cause runtime errors depending on what the lambda actually do. As it may
valid when export is running, also `event` argument will be faked by unsafe cast, so touching it
will cause `ClassCastException`
That is why `unsafeCallConditionalLambdas` flag should be considered as debug/development tool only.
The library provides additional `MetaInfo` objects that might be used along with `unsafeCallConditionalLambdas` flag
to provide complete output (with a help of a user).

* `IgnoreUnsafeCallConditionalLambdasMetaInfo` allows to ignore `unsafeCallConditionalLambdas` flag for some state or
transition
* `ExportMetaInfoBuilder` (is built by `buildExportMetaInfo` function) allows a user to manually specify hint
information (list of hints) for the library to print complete export output.
User is responsible to specify correct information.
There is `StateResolutionHint` which is useful to specify target states (so lambda execution is not needed)
and `EventAndArgumentResolutionHint` allowing to specify `Event` and argument, which will be used to execute a
conditional lambda. This allows to bypass limitations of default behaviour with fake event.

Here are some usage samples:
```kotlin
class ValueEvent(val value: Int) : Event

transitionConditionally<ValueEvent> {
direction = {
when (event.value) {
1 -> targetState(state1)
2 -> targetState(state2)
3 -> targetParallelStates(state1, state2)
4 -> stay()
else -> noTransition()
}
}
metaInfo = buildExportMetaInfo {
resolutionHints = setOf(
// the library does not need to call "direction" lambda, this hint provides the result (state1) directly
StateResolutionHint("when 1", state1),
// calls "direction" lambda during export with specified Event and optional argument (lambda will return state2)
EventAndArgumentResolutionHint("when 2", ValueEvent(2)),
// you can specify set of states that represents parallel target states
StateResolutionHint("when 3", setOf(state1, state2)),
// describes stay() behaviour without calling "direction" lambda
StateResolutionHint("when 4", this@createStateMachine),
// resolves to stay() by calling "direction" lambda
EventAndArgumentResolutionHint("when 4", ValueEvent(4)),
// useless, does not affect export output as it resolves to noTransition()
EventAndArgumentResolutionHint("else", ValueEvent(5)),
)
}
}
```

## PlantUML

Expand Down Expand Up @@ -64,7 +110,7 @@ See [Mermaid nested states export sample](https://github.com/KStateMachine/kstat

## Controlling export output

To beautify and enrich export output, you can use `UmlMetaInfo` for both `IState` and `Transition`. It can be built
To beautify and enrich export output, you can use `UmlMetaInfo` for both `IState` and `Transition`. It can be built
with `buildUmlMetaInfo()` function:

```kotlin
Expand Down
21 changes: 18 additions & 3 deletions docs/pages/meta_information.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,27 @@ title: Meta information

## `metaInfo` property

The library provides `metaInfo` property for `IState` and `Transition` types.
The library declares `metaInfo` property for `IState` and `Transition` types.
`MetaInfo` is a marker interface allowing to attach some static information to library primitives.
This mechanism is extendable and users may add their own `MetaInfo` sub interfaces/classes if necessary.
Currently, the only standard implementation is `UmlMetaInfo` which is useful for export feature.

One of standard implementations is `UmlMetaInfo` which is useful for export feature.
See [controlling export output](https://kstatemachine.github.io/kstatemachine/pages/export.html#controlling-export-output).
You can build it using `buildUmlMetaInfo()` function.

{: .note }
`MetaInfo` considered to be immutable data by design
`MetaInfo` considered to be immutable data by design

## MetaInfo composition

If you need to specify more than one `MetaInfo` subtypes for `IState` or `Transition` you have two options:
1) Use `CompositeMetaInfo` which is constructed by `buildCompositeMetaInfo` builder function.
This type allows to specify a set of `MetaInfo` objects.

Limitations:
* `CompositeMetaInfo` cannot nest into each other. Only one layer is supported.
* Don't try to specify `MetaInfo` of same type multiple times. This is wrong.
Certain `MetaInfo` subtype should be applied only once. Exception will be thrown otherwise.
2) Manually implement all required `MetaInfo` interfaces in a single object.

Both options are supported, choose any one you like more.
52 changes: 51 additions & 1 deletion kstatemachine/api/kstatemachine.api
Original file line number Diff line number Diff line change
Expand Up @@ -91,11 +91,57 @@ public final class ru/nsk/kstatemachine/event/WrappedEvent : ru/nsk/kstatemachin
public final fun getEvent ()Lru/nsk/kstatemachine/event/Event;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/CompositeMetaInfo : ru/nsk/kstatemachine/metainfo/MetaInfo {
public abstract fun getMetaInfoSet ()Ljava/util/Set;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/CompositeMetaInfoBuilder : ru/nsk/kstatemachine/metainfo/CompositeMetaInfo {
public abstract fun getMetaInfoSet ()Ljava/util/Set;
public abstract fun setMetaInfoSet (Ljava/util/Set;)V
}

public final class ru/nsk/kstatemachine/metainfo/EventAndArgumentResolutionHint : ru/nsk/kstatemachine/metainfo/ResolutionHint {
public fun <init> (Ljava/lang/String;Lru/nsk/kstatemachine/event/Event;Ljava/lang/Object;)V
public synthetic fun <init> (Ljava/lang/String;Lru/nsk/kstatemachine/event/Event;Ljava/lang/Object;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
public final fun getArgument ()Ljava/lang/Object;
public final fun getDescription ()Ljava/lang/String;
public final fun getEvent ()Lru/nsk/kstatemachine/event/Event;
public final fun getEventAndArgument ()Lru/nsk/kstatemachine/transition/EventAndArgument;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/ExportMetaInfo : ru/nsk/kstatemachine/metainfo/MetaInfo {
public abstract fun getResolutionHints ()Ljava/util/Set;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/ExportMetaInfoBuilder : ru/nsk/kstatemachine/metainfo/ExportMetaInfo {
public abstract fun getResolutionHints ()Ljava/util/Set;
public abstract fun setResolutionHints (Ljava/util/Set;)V
}

public final class ru/nsk/kstatemachine/metainfo/ExportMetaInfoKt {
public static final fun buildExportMetaInfo (Lkotlin/jvm/functions/Function1;)Lru/nsk/kstatemachine/metainfo/ExportMetaInfo;
}

public final class ru/nsk/kstatemachine/metainfo/IgnoreUnsafeCallConditionalLambdasMetaInfo : ru/nsk/kstatemachine/metainfo/MetaInfo {
public static final field INSTANCE Lru/nsk/kstatemachine/metainfo/IgnoreUnsafeCallConditionalLambdasMetaInfo;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/MetaInfo {
}

public final class ru/nsk/kstatemachine/metainfo/MetaInfoKt {
public static final fun buildUmlMetaInfo (Lkotlin/jvm/functions/Function1;)Lru/nsk/kstatemachine/metainfo/UmlMetaInfo;
public static final fun buildCompositeMetaInfo (Lkotlin/jvm/functions/Function1;)Lru/nsk/kstatemachine/metainfo/CompositeMetaInfo;
public static final fun buildCompositeMetaInfo (Lru/nsk/kstatemachine/metainfo/MetaInfo;Lru/nsk/kstatemachine/metainfo/MetaInfo;[Lru/nsk/kstatemachine/metainfo/MetaInfo;)Lru/nsk/kstatemachine/metainfo/CompositeMetaInfo;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/ResolutionHint {
}

public final class ru/nsk/kstatemachine/metainfo/StateResolutionHint : ru/nsk/kstatemachine/metainfo/ResolutionHint {
public fun <init> (Ljava/lang/String;Ljava/util/Set;)V
public fun <init> (Ljava/lang/String;Lru/nsk/kstatemachine/state/IState;)V
public final fun getDescription ()Ljava/lang/String;
public final fun getTargetStates ()Ljava/util/Set;
}

public abstract interface class ru/nsk/kstatemachine/metainfo/UmlMetaInfo : ru/nsk/kstatemachine/metainfo/MetaInfo {
Expand All @@ -113,6 +159,10 @@ public abstract interface class ru/nsk/kstatemachine/metainfo/UmlMetaInfoBuilder
public abstract fun setUmlStateDescriptions (Ljava/util/List;)V
}

public final class ru/nsk/kstatemachine/metainfo/UmlMetaInfoKt {
public static final fun buildUmlMetaInfo (Lkotlin/jvm/functions/Function1;)Lru/nsk/kstatemachine/metainfo/UmlMetaInfo;
}

public final class ru/nsk/kstatemachine/persistence/EmptyValidator : ru/nsk/kstatemachine/persistence/RestorationResultValidator {
public static final field INSTANCE Lru/nsk/kstatemachine/persistence/EmptyValidator;
public fun validate (Lru/nsk/kstatemachine/persistence/RestorationResult;Lru/nsk/kstatemachine/persistence/RecordedEvents;Lru/nsk/kstatemachine/statemachine/StateMachine;)V
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Author: Mikhail Fedotov
* Github: https://github.com/KStateMachine
* Copyright (c) 2024.
* All rights reserved.
*/

package ru.nsk.kstatemachine.metainfo

import ru.nsk.kstatemachine.event.Event
import ru.nsk.kstatemachine.state.IState
import ru.nsk.kstatemachine.state.RedirectPseudoState
import ru.nsk.kstatemachine.transition.EventAndArgument

/**
* Hint to be used with [ExportMetaInfo]
*/
sealed interface ResolutionHint

/**
* To be used with state/transition constructions where some conditional lambda should return [IState] type.
* If the hint takes an effect (applied correctly), the conditional lambda will not be called even if
* [unsafeCallConditionalLambdas] is true.
* You can specify multiple [StateResolutionHint] instances for the same construction to cover all internal branches.
* User is responsible to provide correct hints.
*/
class StateResolutionHint(
val description: String,
/** Allows to specify parallel target states */
val targetStates: Set<IState>,
) : ResolutionHint {
constructor(
description: String,
targetState: IState,
) : this(description, setOf(targetState))

init {
require(targetStates.isNotEmpty()) {
"targetStates must be non-empty, use single state or multiple states for parallel transitions"
}
}
}

/**
* To be used with state/transition constructions where some conditional lambda uses [EventAndArgument] instance.
* If the hint takes an effect (applied correctly) and [unsafeCallConditionalLambdas] flag is true the conditional lambda
* will be called with specified [EventAndArgument] instead of default fake ([ExportPlantUmlEvent]).
* You can specify multiple [EventAndArgumentResolutionHint] instances for the same construction to cover all internal branches.
* User is responsible to provide correct hints.
*/
class EventAndArgumentResolutionHint(
val description: String,
val event: Event,
val argument: Any? = null
) : ResolutionHint {
val eventAndArgument = EventAndArgument(event, argument)
}

/**
* Allows to ignore an effect of export's feature [unsafeCallConditionalLambdas] flag for certain conditional transition
* or conditional state [RedirectPseudoState]. Conditional lambda will not be called if this meta info is applied.
*/
object IgnoreUnsafeCallConditionalLambdasMetaInfo : MetaInfo

/**
* Standard [MetaInfo], to control unsafe export feature.
*/
interface ExportMetaInfo : MetaInfo {
/**
* Default: emptySet()
*/
val resolutionHints: Set<ResolutionHint>
}

/**
* [ExportMetaInfo] 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 [buildExportMetaInfo] instead.
*/
interface ExportMetaInfoBuilder : ExportMetaInfo {
override var resolutionHints: Set<ResolutionHint>
}

private data class ExportMetaInfoBuilderImpl(
override var resolutionHints: Set<ResolutionHint> = emptySet(),
) : ExportMetaInfoBuilder

fun buildExportMetaInfo(builder: ExportMetaInfoBuilder.() -> Unit): ExportMetaInfo =
ExportMetaInfoBuilderImpl().apply(builder).copy()
Original file line number Diff line number Diff line change
Expand Up @@ -13,52 +13,48 @@ import ru.nsk.kstatemachine.transition.Transition
/**
* Additional static (designed to be immutable) info for library primitives like [IState] [Transition] etc.
* Users may extend this interface to add their own [MetaInfo] implementations.
* Users may combine multiple [MetaInfo] interfaces into one object.
* Users may combine multiple [MetaInfo] derived interfaces into single object or use [CompositeMetaInfo] instead.
*/
interface MetaInfo

/**
* Standard [MetaInfo], to control export PlantUML and Mermaid feature visualization.
* Allows to specify multiple [MetaInfo] objects.
* It might be simpler than constructing single object implementing multiple [MetaInfo] derived interfaces.
* Nesting [CompositeMetaInfo] into each other is not supported.
* Only one instance of certain [MetaInfo] subtype should be specified.
*/
interface UmlMetaInfo : MetaInfo {
interface CompositeMetaInfo : MetaInfo {
/**
* Will be mapped to "long name" for [IState], and a "label" for [Transition]
* Default: null
* Default: emptySet()
*/
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>
val metaInfoSet: Set<MetaInfo>
}

/**
* [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.
* Helper method for getting [MetaInfo] of specified type
*/
interface UmlMetaInfoBuilder : UmlMetaInfo {
override var umlLabel: String?
override var umlStateDescriptions: List<String>
override var umlNotes: List<String>
inline fun <reified M : MetaInfo> MetaInfo?.findMetaInfo(): M? {
return when (this) {
is M -> this
is CompositeMetaInfo -> metaInfoSet.singleOrNull { it is M } as? M
else -> null
}
}

interface CompositeMetaInfoBuilder : CompositeMetaInfo {
override var metaInfoSet: Set<MetaInfo>
}

private data class UmlMetaInfoBuilderImpl(
override var umlLabel: String? = null,
override var umlStateDescriptions: List<String> = emptyList(),
override var umlNotes: List<String> = emptyList(),
) : UmlMetaInfoBuilder
private data class CompositeMetaInfoBuilderImpl(
override var metaInfoSet: Set<MetaInfo> = emptySet()
) : CompositeMetaInfoBuilder

fun buildCompositeMetaInfo(builder: CompositeMetaInfoBuilder.() -> Unit): CompositeMetaInfo =
CompositeMetaInfoBuilderImpl().apply(builder).copy()

fun buildCompositeMetaInfo(metaInfo1: MetaInfo, metaInfo2: MetaInfo, vararg infos: MetaInfo): CompositeMetaInfo =
CompositeMetaInfoBuilderImpl(infos.toMutableSet().apply {
add(metaInfo1)
add(metaInfo2)
})

fun buildUmlMetaInfo(builder: UmlMetaInfoBuilder.() -> Unit): UmlMetaInfo =
UmlMetaInfoBuilderImpl().apply(builder).copy()
Loading

0 comments on commit b5ab6e9

Please sign in to comment.