Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: DSL Visual FSM #28

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package ru.kontur.mobile.visualfsm.dslTests.testFSM

import ru.kontur.mobile.visualfsm.AsyncWorker
import ru.kontur.mobile.visualfsm.Feature
import ru.kontur.mobile.visualfsm.GenerateTransitionsFactory
import ru.kontur.mobile.visualfsm.TransitionCallbacks
import ru.kontur.mobile.visualfsm.dslTests.testFSM.action.TestDSLAction
import ru.kontur.mobile.visualfsm.providers.GeneratedTransitionsFactoryProvider.provideTransitionsFactory

@GenerateTransitionsFactory
class TestDSLFSMFeature(
initialState: TestDSLFSMState,
asyncWorker: AsyncWorker<TestDSLFSMState, TestDSLAction>? = null,
transitionCallbacks: TransitionCallbacks<TestDSLFSMState>? = null,
) : Feature<TestDSLFSMState, TestDSLAction>(
initialState = initialState,
asyncWorker = asyncWorker,
transitionCallbacks = transitionCallbacks,
transitionsFactory = provideTransitionsFactory(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package ru.kontur.mobile.visualfsm.dslTests.testFSM

import ru.kontur.mobile.visualfsm.State

sealed class TestDSLFSMState : State {

data class Initial(
val count: Int,
) : TestDSLFSMState()

sealed class AsyncWorkerState : TestDSLFSMState() {
data object Loading : AsyncWorkerState()
}

data class Loaded(
val count: Int,
) : TestDSLFSMState()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.kontur.mobile.visualfsm.dslTests.testFSM

import ru.kontur.mobile.visualfsm.AsyncWorker
import ru.kontur.mobile.visualfsm.AsyncWorkerTask
import ru.kontur.mobile.visualfsm.dslTests.testFSM.action.TestDSLAction
import kotlin.coroutines.CoroutineContext

class TestDslFSMAsyncWorker(coroutineDispatcher: CoroutineContext) : AsyncWorker<TestDSLFSMState, TestDSLAction>(coroutineDispatcher) {
override fun onNextState(state: TestDSLFSMState): AsyncWorkerTask<TestDSLFSMState> {
return when (state) {
TestDSLFSMState.AsyncWorkerState.Loading -> {
AsyncWorkerTask.ExecuteAndCancelExist(state) {

}
}

is TestDSLFSMState.Initial -> AsyncWorkerTask.Cancel()
is TestDSLFSMState.Loaded -> AsyncWorkerTask.Cancel()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package ru.kontur.mobile.visualfsm.dslTests.testFSM.action

import ru.kontur.mobile.visualfsm.DslAction
import ru.kontur.mobile.visualfsm.Edge
import ru.kontur.mobile.visualfsm.State
import ru.kontur.mobile.visualfsm.dslTests.testFSM.TestDSLFSMState

class StartLoading(
private val count: Int,
) : TestDSLAction() {

internal fun continueLoading() = selfTransition<TestDSLFSMState.AsyncWorkerState>() transform { state -> state }

internal fun continueLoading1() = selfTransition<TestDSLFSMState.AsyncWorkerState> { state -> state }

@Edge("StartLoading")
internal fun startLoadingOther() = transition<TestDSLFSMState.Initial, TestDSLFSMState.AsyncWorkerState.Loading>()
.predicate { count == 0 }
.transform { state -> TestDSLFSMState.AsyncWorkerState.Loading }

@Edge("StartLoading")
internal fun startLoadingOther1() = transition<TestDSLFSMState.Initial, TestDSLFSMState.AsyncWorkerState.Loading>()
.transform { state -> TestDSLFSMState.AsyncWorkerState.Loading }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package ru.kontur.mobile.visualfsm.dslTests.testFSM.action

import ru.kontur.mobile.visualfsm.DslAction
import ru.kontur.mobile.visualfsm.dslTests.testFSM.TestDSLFSMState

sealed class TestDSLAction : DslAction<TestDSLFSMState>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package ru.kontur.mobile.visualfsm.dslTests

import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.advanceUntilIdle
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import ru.kontur.mobile.visualfsm.Action
import ru.kontur.mobile.visualfsm.Transition
import ru.kontur.mobile.visualfsm.TransitionCallbacks
import ru.kontur.mobile.visualfsm.dslTests.testFSM.TestDSLFSMFeature
import ru.kontur.mobile.visualfsm.dslTests.testFSM.TestDSLFSMState
import ru.kontur.mobile.visualfsm.dslTests.testFSM.TestDslFSMAsyncWorker
import ru.kontur.mobile.visualfsm.dslTests.testFSM.action.StartLoading
import ru.kontur.mobile.visualfsm.helper.runFSMFeatureTest

class DSLClassInActionTests {
@OptIn(ExperimentalCoroutinesApi::class)
@Test
fun doSealedFromStateActionTest() = runFSMFeatureTest(
featureFactory = { dispatcher ->
TestDSLFSMFeature(
initialState = TestDSLFSMState.Initial(0),
asyncWorker = TestDslFSMAsyncWorker(dispatcher),
transitionCallbacks = object : TransitionCallbacks<TestDSLFSMState> {
override fun onActionLaunched(action: Action<TestDSLFSMState>, currentState: TestDSLFSMState) {
}

override fun onTransitionSelected(
action: Action<TestDSLFSMState>,
transition: Transition<TestDSLFSMState, TestDSLFSMState>,
currentState: TestDSLFSMState,
) {
}

override fun onNewStateReduced(
action: Action<TestDSLFSMState>,
transition: Transition<TestDSLFSMState, TestDSLFSMState>,
oldState: TestDSLFSMState,
newState: TestDSLFSMState,
) {
}

override fun onNoTransitionError(action: Action<TestDSLFSMState>, currentState: TestDSLFSMState) {
throw IllegalStateException("onNoTransitionError $action $currentState")
}

override fun onMultipleTransitionError(action: Action<TestDSLFSMState>, currentState: TestDSLFSMState) {

}
})

}
) { feature, states ->
feature.proceed(StartLoading(0))
advanceUntilIdle()
assertEquals(
listOf(
TestDSLFSMState.Initial(0),
TestDSLFSMState.AsyncWorkerState.Loading
),
states
)
}

// @Test
// fun generateDigraphTest() {
// val digraph = VisualFSM.generateDigraph(
// baseAction = TestDSLAction::class,
// baseState = TestDSLFSMState::class,
// initialState = TestDSLFSMState.Initial::class
// )
// println(digraph)
// assertEquals(
// "digraph TestDSLFSMStateTransitions {\n" +
// "\"Initial\" []\n" +
// "\"NavigationState.DialogState.Hide\" []\n" +
// "\"NavigationState.DialogState.Show\" []\n" +
// "\"NavigationState.Screen.Back\" []\n" +
// "\"NavigationState.Screen.Next\" []\n" +
// "\"NavigationState.DialogState.Show\" -> \"NavigationState.DialogState.Hide\" [label=\" HideDialog \"]\n" +
// "\"NavigationState.DialogState.Hide\" -> \"Initial\" [label=\" NavigateCompleted \"]\n" +
// "\"NavigationState.DialogState.Show\" -> \"Initial\" [label=\" NavigateCompleted \"]\n" +
// "\"NavigationState.Screen.Back\" -> \"Initial\" [label=\" NavigateCompleted \"]\n" +
// "\"NavigationState.Screen.Next\" -> \"Initial\" [label=\" NavigateCompleted \"]\n" +
// "\"Initial\" -> \"NavigationState.Screen.Back\" [label=\" NavigateBack \"]\n" +
// "\"Initial\" -> \"NavigationState.Screen.Next\" [label=\" NavigateNext \"]\n" +
// "\"NavigationState.DialogState.Hide\" -> \"NavigationState.DialogState.Hide\" [label=\" ObserveChange \"]\n" +
// "\"NavigationState.DialogState.Show\" -> \"NavigationState.DialogState.Show\" [label=\" ObserveChange \"]\n" +
// "\"Initial\" -> \"Initial\" [label=\" ObserveChange \"]\n" +
// "\"Initial\" -> \"NavigationState.DialogState.Show\" [label=\" ShowDialog \"]\n" +
// "}\n", digraph
// )
// }
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,59 +2,111 @@ package annotation_processor

import annotation_processor.functions.KSClassDeclarationFunctions.getAllNestedSealedSubclasses
import annotation_processor.functions.KSClassDeclarationFunctions.getCanonicalClassNameAndLink
import annotation_processor.functions.KSClassDeclarationFunctions.isClassOrSubclassOf
import annotation_processor.functions.KSClassDeclarationFunctions.isSubclassOf
import annotation_processor.functions.KSFunctionDeclarationFunctions.getCanonicalClassNameAndLink
import annotation_processor.transition_wrapper.TransitionKSClassDeclarationWrapper
import annotation_processor.transition_wrapper.TransitionKSFunctionDeclarationWrapper
import annotation_processor.transition_wrapper.TransitionWrapper
import com.google.devtools.ksp.getDeclaredFunctions
import com.google.devtools.ksp.symbol.ClassKind
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.google.devtools.ksp.symbol.Modifier
import ru.kontur.mobile.visualfsm.DslAction
import ru.kontur.mobile.visualfsm.Transition

internal object ActionsWithTransitionsProvider {

internal fun provide(baseActionClassDeclaration: KSClassDeclaration): Map<KSClassDeclaration, List<TransitionKSClassDeclarationWrapper>> {
internal fun provide(baseActionClassDeclaration: KSClassDeclaration): Map<KSClassDeclaration, List<TransitionWrapper>> {

val actionSealedSubclasses = baseActionClassDeclaration.getAllNestedSealedSubclasses()

if (!actionSealedSubclasses.iterator().hasNext()) {
error("Base action class must have subclasses. The \"${baseActionClassDeclaration.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

actionSealedSubclasses.forEach { actionSealedSubclass ->
actionSealedSubclass.getDeclaredFunctions().forEach {
if (it.modifiers.contains(Modifier.OVERRIDE) && it.simpleName.asString() == "getTransitions") {
error("Action must not override getTransitions function. The \"${actionSealedSubclass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
val isDslAction = baseActionClassDeclaration.isClassOrSubclassOf(DslAction::class)

if (!isDslAction) {
actionSealedSubclasses.forEach { actionSealedSubclass ->
actionSealedSubclass.getDeclaredFunctions().forEach {
if (it.modifiers.contains(Modifier.OVERRIDE) && it.simpleName.asString() == "getTransitions") {
error("Action must not override getTransitions function. The \"${actionSealedSubclass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}
}
}
}


return actionSealedSubclasses.associateWith { actionSealedSubclass ->
getTransitions(actionSealedSubclass)
getTransitions(actionClassDeclaration = actionSealedSubclass)
}
}

private fun getTransitions(actionClassDeclaration: KSClassDeclaration): List<TransitionKSClassDeclarationWrapper> {
private fun getTransitions(actionClassDeclaration: KSClassDeclaration): List<TransitionWrapper> {
val isDslAction = actionClassDeclaration.isClassOrSubclassOf(DslAction::class)

val transitionClasses = actionClassDeclaration.declarations.filterIsInstance<KSClassDeclaration>().filter {
it.classKind == ClassKind.CLASS && it.isSubclassOf(Transition::class)
}
return if (isDslAction) {
val transitions = actionClassDeclaration.getDeclaredFunctions()
.filter { function ->
val isTransition = try {
val returnTypeDeclaration = (function.returnType?.resolve()?.declaration as? KSClassDeclaration)
returnTypeDeclaration?.isClassOrSubclassOf(Transition::class) == true
} catch (e: Exception) {
error("\"${function.getCanonicalClassNameAndLink()}\" returns a value with an invalid class name.")
}
isTransition
}

if (!transitionClasses.iterator().hasNext()) {
error("Action must contains transitions as inner classes. The \"${actionClassDeclaration.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}
if (!transitions.iterator().hasNext()) {
error("Action must contains transitions as function. The \"${actionClassDeclaration.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

transitionClasses.forEach { transitionClass ->
if (!transitionClass.modifiers.contains(Modifier.INNER)) {
error("Transition must have \"inner\" modifier. The \"${transitionClass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
val wrappedTransitions = transitions.map { transitionFunction ->
if (Modifier.PRIVATE in transitionFunction.modifiers) {
error("Transition must not have \"private\" modifier. The \"${transitionFunction.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

if (Modifier.PROTECTED in transitionFunction.modifiers) {
error("Transition must not have \"protected\" modifier. The \"${transitionFunction.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

if (Modifier.SUSPEND in transitionFunction.modifiers) {
error("Transition must not have \"suspend\" modifier. The \"${transitionFunction.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

if (transitionFunction.parameters.isNotEmpty()) {
error("Transition must not have constructor parameters. The \"${transitionFunction.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

TransitionKSFunctionDeclarationWrapper(transitionFunction)
}
if (Modifier.ABSTRACT in transitionClass.modifiers) {
error("Transition must not have \"abstract\" modifier. The \"${transitionClass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")

wrappedTransitions.toList()
} else {
val transitionClasses = actionClassDeclaration.declarations.filterIsInstance<KSClassDeclaration>().filter {
it.classKind == ClassKind.CLASS && it.isSubclassOf(Transition::class)
}
if (transitionClass.primaryConstructor!!.parameters.isNotEmpty()) {
error("Transition must not have constructor parameters. The \"${transitionClass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")

if (!transitionClasses.iterator().hasNext()) {
error("Action must contains transitions as inner classes. The \"${actionClassDeclaration.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}
}

return transitionClasses.toList().map { TransitionKSClassDeclarationWrapper(it) }
}
val wrappedTransitions = transitionClasses.map { transitionClass ->
if (!transitionClass.modifiers.contains(Modifier.INNER)) {
error("Transition must have \"inner\" modifier. The \"${transitionClass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}
if (Modifier.ABSTRACT in transitionClass.modifiers) {
error("Transition must not have \"abstract\" modifier. The \"${transitionClass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}
if (transitionClass.primaryConstructor!!.parameters.isNotEmpty()) {
error("Transition must not have constructor parameters. The \"${transitionClass.getCanonicalClassNameAndLink()}\" does not meet this requirement.")
}

TransitionKSClassDeclarationWrapper(transitionClass)
}

wrappedTransitions.toList()
}
}
}
Original file line number Diff line number Diff line change
@@ -1,38 +1,27 @@
package annotation_processor

import annotation_processor.functions.KSClassDeclarationFunctions.getAllNestedSealedSubclasses
import annotation_processor.functions.KSClassDeclarationFunctions.isClassOrSubclassOf
import annotation_processor.functions.KSClassDeclarationFunctions.simpleStateNameWithSealedName
import annotation_processor.transition_wrapper.TransitionWrapper
import com.google.devtools.ksp.symbol.KSClassDeclaration
import com.squareup.kotlinpoet.asClassName
import com.squareup.kotlinpoet.ksp.toClassName
import ru.kontur.mobile.visualfsm.Edge
import ru.kontur.mobile.visualfsm.SelfTransition

object AllTransitionsListProvider {

fun provide(
baseStateClassDeclaration: KSClassDeclaration,
transitionWrappers: List<TransitionKSClassDeclarationWrapper>,
transitionWrappers: List<TransitionWrapper>,
): String {
val result = mutableListOf<String>()
transitionWrappers.forEach { transitionWrapper ->
val edgeName = transitionWrapper.transitionClassDeclaration
.annotations
.firstOrNull { it.shortName.getShortName() == Edge::class.asClassName().simpleName }
?.arguments
?.firstOrNull { it.name?.getShortName() == "name" }
?.value
?.toString()
?: transitionWrapper.transitionClassDeclaration.toClassName().simpleName
val edgeName = transitionWrapper.edgeName
val fromStates = transitionWrapper.fromState.getAllNestedSealedSubclasses().ifEmpty {
sequenceOf(transitionWrapper.fromState)
}
val toStates = transitionWrapper.toState.getAllNestedSealedSubclasses().ifEmpty {
sequenceOf(transitionWrapper.toState)
}
val transitionClassDeclaration = transitionWrapper.transitionClassDeclaration
val isSelfTransition = transitionClassDeclaration.isClassOrSubclassOf(SelfTransition::class)
val isSelfTransition = transitionWrapper.isClassOrSubclassOf(SelfTransition::class)
fromStates.forEach { fromStateClass ->
val fromStateName = fromStateClass.simpleStateNameWithSealedName(baseStateClassDeclaration)
val filteredToStates = if (isSelfTransition) {
Expand Down
Loading