diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
new file mode 100644
index 0000000..feeb1a9
--- /dev/null
+++ b/.github/workflows/publish-release.yml
@@ -0,0 +1,140 @@
+name: Library Release Deploy
+
+on:
+ push:
+ branches: [ "main" ]
+ workflow_dispatch:
+
+env:
+ GITHUB_USERNAME: "meetacy"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+jobs:
+
+ deploy-multiplatform:
+ runs-on: ubuntu-latest
+ outputs:
+ release_version: ${{ steps.output_version.outputs.release_version }}
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ release_version=$(./gradlew printVersion -q)
+ echo "release_version=$release_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.release_version }}
+ run: ./gradlew publishKotlinMultiplatformPublicationToGitHubRepository
+ - name: Add Sdk Version to Output
+ id: output_version
+ run: echo "release_version=${{ env.release_version }}" >> $GITHUB_OUTPUT
+
+ deploy-jvm:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ release_version=$(./gradlew printVersion -q)
+ echo "release_version=$release_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.release_version }}
+ run: ./gradlew publishJvmPublicationToGitHubRepository
+
+ deploy-android:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ release_version=$(./gradlew printVersion -q)
+ echo "release_version=$release_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.release_version }}
+ run: ./gradlew publishAndroidPublicationToGitHubRepository
+
+ deploy-js:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ release_version=$(./gradlew printVersion -q)
+ echo "release_version=$release_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.release_version }}
+ run: ./gradlew publishJsPublicationToGitHubRepository
+
+ deploy-konan:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ - name: Konan Cache Setup
+ uses: actions/cache@v3
+ with:
+ path: ~/.konan
+ key: konan-cache
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ release_version=$(./gradlew printVersion -q)
+ echo "release_version=$release_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.release_version }}
+ run: |
+ ./gradlew publishIosX64PublicationToGitHubRepository \
+ publishIosSimulatorArm64PublicationToGitHubRepository \
+ publishIosArm64PublicationToGitHubRepository
+
+ create-release:
+ runs-on: ubuntu-latest
+ needs:
+ - deploy-multiplatform
+ - deploy-jvm
+ - deploy-js
+ - deploy-konan
+ - deploy-android
+ steps:
+ - name: Create Release
+ uses: actions/create-release@v1
+ with:
+ tag_name: ${{ needs.deploy-multiplatform.outputs.release_version }}
+ release_name: Release ${{ needs.deploy-multiplatform.outputs.release_version }}
diff --git a/.github/workflows/publish-snapshot.yml b/.github/workflows/publish-snapshot.yml
new file mode 100644
index 0000000..3e4f8de
--- /dev/null
+++ b/.github/workflows/publish-snapshot.yml
@@ -0,0 +1,132 @@
+name: Library Snapshot Deploy
+
+on:
+ push:
+ branches-ignore: [ "main" ]
+ workflow_dispatch:
+
+env:
+ GITHUB_USERNAME: "meetacy"
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ BRANCH_NAME: ${{ github.head_ref || github.ref_name }}
+ ORG_GRADLE_PROJECT_snapshot: true
+ ORG_GRADLE_PROJECT_commit: ${{ github.sha }}
+ ORG_GRADLE_PROJECT_attempt: ${{ github.run_attempt }}
+
+jobs:
+
+ deploy-multiplatform:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/dev' }}
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ snapshot_version=$(./gradlew printVersion -q)
+ echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.snapshot_version }}
+ run: ./gradlew publishKotlinMultiplatformPublicationToMeetacySdkRepository
+
+ deploy-jvm:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/dev' }}
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ snapshot_version=$(./gradlew printVersion -q)
+ echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.snapshot_version }}
+ run: ./gradlew publishJvmPublicationToMeetacySdkRepository
+
+ deploy-android:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ release_version=$(./gradlew printVersion -q)
+ echo "release_version=$release_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.release_version }}
+ run: ./gradlew publishAndroidPublicationToGitHubRepository
+
+ deploy-js:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/dev' }}
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ snapshot_version=$(./gradlew printVersion -q)
+ echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.snapshot_version }}
+ run: ./gradlew publishJsPublicationToMeetacySdkRepository
+
+ deploy-konan:
+ runs-on: macos-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Setup Java
+ uses: actions/setup-java@v3
+ with:
+ distribution: temurin
+ java-version: 11
+ - name: Gradle Cache Setup
+ uses: gradle/gradle-build-action@v2
+ with:
+ cache-read-only: ${{ github.ref != 'refs/heads/dev' }}
+ - name: Konan Cache Setup
+ uses: actions/cache@v3
+ with:
+ path: ~/.konan
+ key: konan-cache
+ - name: Gradle Sync
+ run: ./gradlew
+ - name: Add Version to Env
+ run: |
+ snapshot_version=$(./gradlew printVersion -q)
+ echo "snapshot_version=$snapshot_version" >> $GITHUB_ENV
+ - name: Publish ${{ env.snapshot_version }}
+ run: |
+ ./gradlew publishIosX64PublicationToMeetacySdkRepository \
+ publishIosSimulatorArm64PublicationToMeetacySdkRepository \
+ publishIosArm64PublicationToMeetacySdkRepository
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..9d14494
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,9 @@
+.idea
+build
+.gradle
+.kotlin
+kotlin-js-store
+local.properties
+**/.DS_Store
+*/**/xcuserdata
+**/*xcworkspace
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..567c59c
--- /dev/null
+++ b/README.md
@@ -0,0 +1,219 @@
+# Kotlin State Machine
+
+## Overall Idea
+
+This library tries to unify approach of different implications of
+state machines. For now, considered cases to support are:
+
+- Multiplatform Navigation (With convenient native wrappers)
+- Native Navigation for android/iOS (Without redundant entities needed for multiplatform)
+- FSM for telegram bots
+
+Even though FSM for telegram bots and Navigation seems to be completely
+different things they actually do share lots of common things such as:
+
+- Coroutines Integration (both need to start/cancel scope when start/finish state and structured concurrency when state starts child state)
+- Serialization Integration (both need to save entries when state is resumed =
+either the state was just created or it's child was finished and the state become top state)
+- Stack Integration (both have the same stack idea which is also shared between them)
+
+## Plugins
+
+All functionality of StateController is extended using Plugins. Plugin is just a simple interface with
+the only method 'install(context: Context): Context' which creates a new context with plugin
+installed in it.
+
+There are some core principles and idioms of writing plugins.
+
+
+// TODO:
+
+- plugin and it's internal things under 'plugin' folder
+- public apis under feature folder
+- you should NEVER append elements to context except for context interceptors (otherwise onResume will work incorrect)
+
+## WIP
+
+Some WIP docs are under [docs folder](docs).
+
+## Example
+
+### Telegram Bots
+
+
+ Example
+
+```kotlin
+package ksm.ktgbotapi.example
+
+import dev.inmo.tgbotapi.bot.TelegramBot
+import dev.inmo.tgbotapi.extensions.utils.types.buttons.replyKeyboard
+import dev.inmo.tgbotapi.types.buttons.SimpleKeyboardButton
+import dev.inmo.tgbotapi.types.message.abstracts.PrivateContentMessage
+import dev.inmo.tgbotapi.types.update.MessageUpdate
+import kotlinx.coroutines.flow.Flow
+import ksm.context.finish
+import ksm.finish
+import ksm.kotlinx.serialization.plugin.KotlinxSerializationPlugin
+import ksm.ktgbotapi.*
+import ksm.ktgbotapi.match.command
+import ksm.ktgbotapi.match.exact
+import ksm.state.builder.StateRouteScope
+import ksm.state.builder.states
+import ksm.state.launch
+import ksm.state.name.named
+
+const val MAIN_STATE = "MainState"
+const val MAIN_MENU_STATE = "MainMenuState"
+const val STATE_A = "MainMenuState"
+const val STATE_B = "MainMenuState"
+
+val MessageUpdate.peerKey: TelegramPeerKey get() {
+ val message = data as PrivateContentMessage<*>
+ val id = message.chat.id
+ return TelegramPeerKey(id.chatId.long.toString())
+}
+
+suspend fun start(
+ bot: TelegramBot,
+ updates: Flow
+) {
+ val fsm = TelegramBotStateMachine {
+ install(KotlinxSerializationPlugin())
+
+ states {
+ main()
+ mainMenu()
+ stateA()
+ stateB()
+ }
+ }
+
+ fsm.start(
+ startStateName = MAIN_STATE,
+ telegramBot = bot,
+ updates = updates,
+ key = MessageUpdate::peerKey
+ )
+}
+
+fun StateRouteScope.main() = named(MAIN_STATE) {
+ execute {
+ sendMessage(
+ text = "Hello, ${user.firstName}! Choose an option using buttons below:",
+ replyMarkup = replyKeyboard(oneTimeKeyboard = true) {
+ +SimpleKeyboardButton(text = "Launch StateA")
+ +SimpleKeyboardButton(text = "Launch StateB")
+ }
+ )
+ controller.launch(MAIN_MENU_STATE)
+ }
+}
+
+fun StateRouteScope.mainMenu() = named(MAIN_MENU_STATE) {
+ execute {
+ matchMessage {
+ exact("Launch StateA") { controller.launch(STATE_A) }
+ exact("Launch StateB") { controller.launch(STATE_B) }
+ command("cancel") { controller.finish() }
+ }
+ }
+}
+
+fun StateRouteScope.stateA() = named(STATE_A) {
+ execute { sendMessage("StateA!") }
+}
+
+fun StateRouteScope.stateB() = named(STATE_B) {
+ execute { sendMessage("StateB!") }
+}
+```
+
+
+
+### Compose
+
+
+ Example
+
+```kotlin
+package ksm.compose.example
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import app.meetacy.di.DI
+import ksm.mdi.di
+import ksm.mdi.plugin.DIPlugin
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.emptyFlow
+import ksm.compose.Content
+import ksm.compose.example.MainViewModel.Action
+import ksm.compose.host.StateHost
+import ksm.compose.rememberStateController
+import ksm.kotlinx.serialization.plugin.KotlinxSerializationPlugin
+import ksm.state.builder.StateRouteScope
+import ksm.state.builder.states
+import ksm.state.data.receive
+import ksm.state.launch
+import ksm.state.name.named
+
+class MainViewModel {
+ val actions: Flow = emptyFlow()
+
+ sealed interface Action {
+ data object RouteDetails : Action
+ }
+}
+
+data class DetailsParameters(val info: String)
+
+const val MAIN_STATE = "MainState"
+const val DETAILS_STATE = "DetailsState"
+
+@Composable
+fun AppContent(di: DI) {
+ val controller = rememberStateController {
+ install(KotlinxSerializationPlugin())
+ install(DIPlugin(di))
+
+ states {
+ main()
+ details()
+ }
+ }
+
+ StateHost(
+ controller = controller,
+ startStateName = MAIN_STATE
+ )
+}
+
+fun StateRouteScope.main() = named(MAIN_STATE) {
+ Content {
+ val viewModel: MainViewModel = controller.di.viewModel()
+
+ LaunchedEffect(viewModel) {
+ viewModel.actions.collect { action ->
+ when (action) {
+ Action.RouteDetails -> controller.launch(
+ name = DETAILS_STATE,
+ data = DetailsParameters(info = "Test")
+ )
+ }
+ }
+ }
+ }
+}
+
+fun StateRouteScope.details() = named(DETAILS_STATE) {
+ Content {
+ val parameters: DetailsParameters = controller.receive()
+
+ LaunchedEffect(Unit) {
+ println(parameters)
+ }
+ }
+}
+```
+
+
diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts
new file mode 100644
index 0000000..60a2a2b
--- /dev/null
+++ b/build-logic/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ `kotlin-dsl`
+}
+
+dependencies {
+ api(libs.kotlin.gradle.plugin)
+ api(libs.kotlinx.serialization.gradle.plugin)
+ api(libs.android.gradle.plugin)
+}
diff --git a/build-logic/settings.gradle.kts b/build-logic/settings.gradle.kts
new file mode 100644
index 0000000..ae470e1
--- /dev/null
+++ b/build-logic/settings.gradle.kts
@@ -0,0 +1,12 @@
+dependencyResolutionManagement {
+ repositories {
+ mavenCentral()
+ google()
+ }
+
+ versionCatalogs {
+ create("libs") {
+ from(files("../gradle/libs.versions.toml"))
+ }
+ }
+}
diff --git a/build-logic/src/main/kotlin/Version.kt b/build-logic/src/main/kotlin/Version.kt
new file mode 100644
index 0000000..719ec1a
--- /dev/null
+++ b/build-logic/src/main/kotlin/Version.kt
@@ -0,0 +1,26 @@
+import org.gradle.api.Project
+
+fun Project.versionFromProperties(acceptor: (String) -> Unit) {
+ afterEvaluate {
+ acceptor(versionFromProperties())
+ }
+}
+
+fun Project.versionFromProperties(): String {
+ val snapshot = project.findProperty("snapshot")?.toString()?.toBooleanStrict()
+ if (snapshot == null || !snapshot) return project.version.toString()
+
+ val commit = project.property("commit").toString()
+ val attempt = project.property("attempt").toString().toInt()
+
+ val version = buildString {
+ append(project.version)
+ append("-build")
+ append(commit.take(n = 7))
+ if (attempt > 1) {
+ append(attempt)
+ }
+ }
+
+ return version
+}
diff --git a/build-logic/src/main/kotlin/android-library-convention.gradle.kts b/build-logic/src/main/kotlin/android-library-convention.gradle.kts
new file mode 100644
index 0000000..2081e18
--- /dev/null
+++ b/build-logic/src/main/kotlin/android-library-convention.gradle.kts
@@ -0,0 +1,47 @@
+plugins {
+ id("com.android.library")
+ kotlin("android")
+ id("publication-convention")
+}
+
+android {
+ compileSdk = 33
+
+ defaultConfig {
+ minSdk = 21
+ targetSdk = 33
+ }
+
+ buildFeatures {
+ buildConfig = false
+ }
+
+ publishing {
+ singleVariant("release") {
+ withSourcesJar()
+ }
+ }
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_1_8
+ targetCompatibility = JavaVersion.VERSION_1_8
+ }
+}
+
+publishing {
+ publications {
+ register("android") {
+ groupId = "app.meetacy.ksm"
+ artifactId = project.name
+ afterEvaluate {
+ version = versionFromProperties()
+ from(components["release"])
+ }
+ }
+ }
+}
+
+kotlin {
+ explicitApi()
+ jvmToolchain(8)
+}
diff --git a/build-logic/src/main/kotlin/kmp-library-convention.gradle.kts b/build-logic/src/main/kotlin/kmp-library-convention.gradle.kts
new file mode 100644
index 0000000..a0bdddc
--- /dev/null
+++ b/build-logic/src/main/kotlin/kmp-library-convention.gradle.kts
@@ -0,0 +1,20 @@
+plugins {
+ kotlin("multiplatform")
+ id("publication-convention")
+}
+
+kotlin {
+ explicitApi()
+
+ jvmToolchain(8)
+
+ jvm()
+
+ js(IR) {
+ browser()
+ nodejs()
+ }
+ iosArm64()
+ iosX64()
+ iosSimulatorArm64()
+}
diff --git a/build-logic/src/main/kotlin/print-version-convention.gradle.kts b/build-logic/src/main/kotlin/print-version-convention.gradle.kts
new file mode 100644
index 0000000..b31e74a
--- /dev/null
+++ b/build-logic/src/main/kotlin/print-version-convention.gradle.kts
@@ -0,0 +1,13 @@
+@file:Suppress("UNUSED_VARIABLE")
+
+import org.gradle.kotlin.dsl.creating
+
+tasks {
+ val printVersion by creating {
+ group = "CI"
+
+ doFirst {
+ println(versionFromProperties())
+ }
+ }
+}
diff --git a/build-logic/src/main/kotlin/publication-convention.gradle.kts b/build-logic/src/main/kotlin/publication-convention.gradle.kts
new file mode 100644
index 0000000..b6d92bc
--- /dev/null
+++ b/build-logic/src/main/kotlin/publication-convention.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ id("org.gradle.maven-publish")
+}
+
+group = "app.meetacy.ksm"
+
+publishing {
+ repositories {
+ maven {
+ name = "GitHub"
+ url = uri("https://maven.pkg.github.com/meetacy/ksm")
+ credentials {
+ username = System.getenv("GITHUB_USERNAME")
+ password = System.getenv("GITHUB_TOKEN")
+ }
+ }
+ }
+
+ publications.withType {
+ versionFromProperties { version ->
+ this.version = version
+ }
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
new file mode 100644
index 0000000..6b123e4
--- /dev/null
+++ b/build.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("print-version-convention")
+}
+
+version = libs.versions.ksm.get()
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
new file mode 100644
index 0000000..e396a49
--- /dev/null
+++ b/core/build.gradle.kts
@@ -0,0 +1,5 @@
+plugins {
+ id("kmp-library-convention")
+}
+
+version = libs.versions.ksm.get()
diff --git a/core/src/commonMain/kotlin/ksm/StateController.kt b/core/src/commonMain/kotlin/ksm/StateController.kt
new file mode 100644
index 0000000..7f9205a
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/StateController.kt
@@ -0,0 +1,57 @@
+package ksm
+
+import ksm.builder.StateControllerBuilder
+import ksm.builtin.HasBuiltinPluginsPlugin
+import ksm.context.StateContext
+import ksm.context.configuration.plugin.ConfigurationPlugin
+import ksm.context.finish
+import ksm.finish.once.plugin.FinishOncePlugin
+import ksm.lifecycle.plugin.LifecyclePlugin
+import ksm.stack.plugin.StateStackPlugin
+import ksm.state.name.plugin.StateNamePlugin
+import ksm.state.parameters.plugin.StateParametersPlugin
+
+public inline fun createRawStateController(
+ context: StateContext = StateContext.Empty,
+ enableStateName: Boolean = true,
+ enableStateParameters: Boolean = true,
+ enableConfiguration: Boolean = true,
+ enableLifecycle: Boolean = true,
+ enableStateStack: Boolean = true,
+ enableFinishOnce: Boolean = true,
+ builder: StateControllerBuilder.() -> Unit = {}
+): StateController {
+ var applied = context
+
+ applied = StateControllerBuilder(applied).apply {
+ if (HasBuiltinPluginsPlugin in context) return@apply
+
+ // Installing built-in features
+ if (enableStateName) install(StateNamePlugin)
+ if (enableStateParameters) install(StateParametersPlugin)
+ if (enableConfiguration) install(ConfigurationPlugin)
+ if (enableLifecycle) install(LifecyclePlugin)
+ if (enableStateStack) install(StateStackPlugin)
+ if (enableFinishOnce) install(FinishOncePlugin)
+
+ install(HasBuiltinPluginsPlugin)
+ }.apply(builder).build()
+
+ return object : StateController {
+ override val context = applied
+ }
+}
+
+public fun StateController(context: StateContext): StateController {
+ return object : StateController {
+ override val context = context
+ }
+}
+
+public interface StateController {
+ public val context: StateContext
+}
+
+public fun StateController.finish() {
+ context.finish()
+}
diff --git a/core/src/commonMain/kotlin/ksm/annotation/MutateContext.kt b/core/src/commonMain/kotlin/ksm/annotation/MutateContext.kt
new file mode 100644
index 0000000..d89c855
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/annotation/MutateContext.kt
@@ -0,0 +1,7 @@
+package ksm.annotation
+
+@RequiresOptIn(
+ message = "Indicates that the following function creates a copy of context. " +
+ "Do not opt-in! Only propagate this annotation"
+)
+public annotation class MutateContext
diff --git a/core/src/commonMain/kotlin/ksm/annotation/StateBuilderDSL.kt b/core/src/commonMain/kotlin/ksm/annotation/StateBuilderDSL.kt
new file mode 100644
index 0000000..449dfea
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/annotation/StateBuilderDSL.kt
@@ -0,0 +1,4 @@
+package ksm.annotation
+
+@DslMarker
+public annotation class StateBuilderDSL
diff --git a/core/src/commonMain/kotlin/ksm/builder/StateControllerBuilder.kt b/core/src/commonMain/kotlin/ksm/builder/StateControllerBuilder.kt
new file mode 100644
index 0000000..e55a251
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/builder/StateControllerBuilder.kt
@@ -0,0 +1,18 @@
+package ksm.builder
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.install
+import ksm.plugin.Plugin
+
+public class StateControllerBuilder(public var context: StateContext) {
+
+ @OptIn(MutateContext::class)
+ public fun install(plugin: Plugin) {
+ context = context.install(plugin)
+ }
+
+ public fun build(): StateContext {
+ return context
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/builtin/HasBuiltinPluginsPlugin.kt b/core/src/commonMain/kotlin/ksm/builtin/HasBuiltinPluginsPlugin.kt
new file mode 100644
index 0000000..61030cb
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/builtin/HasBuiltinPluginsPlugin.kt
@@ -0,0 +1,12 @@
+package ksm.builtin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.plugin.Plugin
+
+public object HasBuiltinPluginsPlugin : Plugin.Singleton {
+
+ // HasBuiltinPluginsPlugin is installed and added to context automatically
+ @MutateContext
+ override fun install(context: StateContext): StateContext = context
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/CombinedContext.kt b/core/src/commonMain/kotlin/ksm/context/CombinedContext.kt
new file mode 100644
index 0000000..1149f54
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/CombinedContext.kt
@@ -0,0 +1,22 @@
+package ksm.context
+
+import ksm.annotation.MutateContext
+
+/**
+ * [right] elements overwrite [left] elements
+ */
+internal class CombinedContext(
+ val left: StateContext,
+ val right: StateContext
+) : StateContext {
+ override val keys = left.keys + right.keys
+
+ override fun get(key: StateContext.Key): T? {
+ return right[key] ?: left[key]
+ }
+
+ @MutateContext
+ override fun minus(key: StateContext.Key<*>): StateContext {
+ return CombinedContext(left.minus(key), right.minus(key))
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/CreateChildStateContext.kt b/core/src/commonMain/kotlin/ksm/context/CreateChildStateContext.kt
new file mode 100644
index 0000000..8e55629
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/CreateChildStateContext.kt
@@ -0,0 +1,23 @@
+package ksm.context
+
+import ksm.annotation.MutateContext
+import ksm.context.configuration.plugin.ConfigurationPlugin
+import ksm.lifecycle.plugin.LifecyclePlugin
+
+@OptIn(MutateContext::class)
+public inline fun StateContext.createChildContext(
+ setup: (StateContext) -> Unit
+): StateContext {
+ this[LifecyclePlugin]?.onPause(context = this)
+
+ val applied = this[ConfigurationPlugin]
+ ?.onConfigure(context = this)
+ ?: this
+
+ applied.apply(setup)
+
+ applied[LifecyclePlugin]?.onCreate(applied)
+ applied[LifecyclePlugin]?.onResume(applied)
+
+ return applied
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/FinishStateContext.kt b/core/src/commonMain/kotlin/ksm/context/FinishStateContext.kt
new file mode 100644
index 0000000..8b5bfdf
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/FinishStateContext.kt
@@ -0,0 +1,14 @@
+package ksm.context
+
+import ksm.finish.once.plugin.FinishOncePlugin
+import ksm.stack.previousContextOrNull
+import ksm.lifecycle.plugin.LifecyclePlugin
+
+public fun StateContext.finish(): StateContext {
+ this[FinishOncePlugin]?.finish(context = this)
+ this[LifecyclePlugin]?.onPause(context = this)
+ this[LifecyclePlugin]?.onFinish(context = this)
+ val previousContext = previousContextOrNull ?: return this
+ previousContext[LifecyclePlugin]?.onResume(context = previousContext)
+ return this
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/InstallPlugin.kt b/core/src/commonMain/kotlin/ksm/context/InstallPlugin.kt
new file mode 100644
index 0000000..8326e3b
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/InstallPlugin.kt
@@ -0,0 +1,9 @@
+package ksm.context
+
+import ksm.annotation.MutateContext
+import ksm.plugin.Plugin
+
+@MutateContext
+public fun StateContext.install(plugin: Plugin): StateContext {
+ return plugin.install(context = this + plugin)
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/SetupStateContext.kt b/core/src/commonMain/kotlin/ksm/context/SetupStateContext.kt
new file mode 100644
index 0000000..13a7da8
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/SetupStateContext.kt
@@ -0,0 +1,16 @@
+package ksm.context
+
+import ksm.annotation.MutateContext
+
+@MutateContext
+public inline fun StateContext.plus(
+ vararg elements: StateContext.Element,
+ block: StateContext.() -> Unit = {}
+): StateContext {
+ val result = elements.fold(
+ initial = this,
+ operation = StateContext::plus
+ )
+ block(result)
+ return result
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/SingleElementContext.kt b/core/src/commonMain/kotlin/ksm/context/SingleElementContext.kt
new file mode 100644
index 0000000..7bfc9d1
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/SingleElementContext.kt
@@ -0,0 +1,25 @@
+package ksm.context
+
+import ksm.annotation.MutateContext
+
+public fun StateContext.Element.asContext(): StateContext {
+ return SingleElementContext(element = this)
+}
+
+private class SingleElementContext(val element: StateContext.Element): StateContext {
+ override val keys = setOf(element.key)
+
+ @Suppress("UNCHECKED_CAST")
+ override fun get(key: StateContext.Key): T? {
+ return if (element.key == key) (this as T) else null
+ }
+
+ @MutateContext
+ override fun minus(key: StateContext.Key<*>): StateContext {
+ return if (key == element.key) {
+ StateContext.Empty
+ } else {
+ this
+ }
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/StateContext.kt b/core/src/commonMain/kotlin/ksm/context/StateContext.kt
new file mode 100644
index 0000000..98e5310
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/StateContext.kt
@@ -0,0 +1,57 @@
+package ksm.context
+
+import ksm.annotation.MutateContext
+
+public interface StateContext {
+ /**
+ * Contract: Variable should be constant
+ */
+ public val keys: Set>
+
+ public operator fun get(key: Key): T?
+ public fun require(key: Key): T = get(key) ?: error("Cannot find element with key $key")
+
+ public operator fun contains(key: Key<*>): Boolean = get(key) != null
+
+ @MutateContext
+ public operator fun plus(other: Element): StateContext {
+ return plus(other.asContext())
+ }
+
+ @MutateContext
+ public operator fun plus(other: StateContext): StateContext {
+ if (this === Empty) return other
+ return CombinedContext(left = this, right = other)
+ }
+
+ @MutateContext
+ public operator fun minus(key: Key<*>): StateContext
+
+ public interface Key
+
+ public interface Element {
+ public val key: Key<*>
+
+ public interface Singleton> : Element, Key {
+ override val key: Key get() = this
+ }
+ }
+
+ public object Empty : StateContext {
+ override val keys: Set> = emptySet()
+ override fun get(key: Key): T? = null
+ @MutateContext
+ override fun minus(key: Key<*>): StateContext = this
+ }
+}
+
+
+public operator fun StateContext?.get(key: StateContext.Key): T? {
+ return this?.get(key)
+}
+
+public operator fun StateContext?.contains(key: StateContext.Key<*>): Boolean {
+ return this?.contains(key) ?: false
+}
+
+public fun StateContext?.orEmpty(): StateContext = this ?: StateContext.Empty
diff --git a/core/src/commonMain/kotlin/ksm/context/configuration/interceptor/ConfigurationInterceptor.kt b/core/src/commonMain/kotlin/ksm/context/configuration/interceptor/ConfigurationInterceptor.kt
new file mode 100644
index 0000000..0c19ca2
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/configuration/interceptor/ConfigurationInterceptor.kt
@@ -0,0 +1,10 @@
+package ksm.context.configuration.interceptor
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+
+public fun interface ConfigurationInterceptor {
+
+ @MutateContext
+ public fun onConfigure(context: StateContext): StateContext
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/configuration/interceptor/StateContext.kt b/core/src/commonMain/kotlin/ksm/context/configuration/interceptor/StateContext.kt
new file mode 100644
index 0000000..0fb080b
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/configuration/interceptor/StateContext.kt
@@ -0,0 +1,12 @@
+package ksm.context.configuration.interceptor
+
+import ksm.context.StateContext
+import ksm.context.configuration.plugin.ConfigurationPlugin
+import ksm.plugin.plugin
+
+public fun StateContext.addConfigurationInterceptor(interceptor: ConfigurationInterceptor) {
+ plugin(ConfigurationPlugin).addConfigurationInterceptor(
+ context = this,
+ interceptor = interceptor
+ )
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/configuration/plugin/ConfigurationPlugin.kt b/core/src/commonMain/kotlin/ksm/context/configuration/plugin/ConfigurationPlugin.kt
new file mode 100644
index 0000000..9edbabb
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/configuration/plugin/ConfigurationPlugin.kt
@@ -0,0 +1,26 @@
+package ksm.context.configuration.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.plugin.Plugin
+
+public object ConfigurationPlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ return context + ConfigurationStateController()
+ }
+
+ public fun addConfigurationInterceptor(
+ context: StateContext,
+ interceptor: ConfigurationInterceptor
+ ) {
+ context.require(ConfigurationStateController).addInterceptor(interceptor)
+ }
+
+ @MutateContext
+ public fun onConfigure(context: StateContext): StateContext {
+ return context.require(ConfigurationStateController).onConfigure(context)
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/context/configuration/plugin/ConfigurationStateController.kt b/core/src/commonMain/kotlin/ksm/context/configuration/plugin/ConfigurationStateController.kt
new file mode 100644
index 0000000..65ee286
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/context/configuration/plugin/ConfigurationStateController.kt
@@ -0,0 +1,22 @@
+package ksm.context.configuration.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+
+internal class ConfigurationStateController : StateContext.Element {
+ override val key = ConfigurationStateController
+
+ private val interceptors = mutableListOf()
+
+ fun addInterceptor(interceptor: ConfigurationInterceptor) {
+ interceptors += interceptor
+ }
+
+ @MutateContext
+ fun onConfigure(context: StateContext): StateContext {
+ return interceptors.fold(context) { acc, interceptor -> interceptor.onConfigure(acc) }
+ }
+
+ companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/finish/once/plugin/FinishOnceEntry.kt b/core/src/commonMain/kotlin/ksm/finish/once/plugin/FinishOnceEntry.kt
new file mode 100644
index 0000000..f960ac4
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/finish/once/plugin/FinishOnceEntry.kt
@@ -0,0 +1,20 @@
+package ksm.finish.once.plugin
+
+import ksm.context.StateContext
+
+internal class FinishOnceEntry : StateContext.Element {
+ override val key = FinishOncePlugin
+
+ private var isFinished: Boolean = false
+
+ fun checkCanCreate() {
+ if (isFinished) error("Current state was already finished, cannot create a new one")
+ }
+
+ fun finish() {
+ if (isFinished) error("Current state was already finished, cannot finish it twice")
+ isFinished = true
+ }
+
+ companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/finish/once/plugin/FinishOncePlugin.kt b/core/src/commonMain/kotlin/ksm/finish/once/plugin/FinishOncePlugin.kt
new file mode 100644
index 0000000..e78f9ec
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/finish/once/plugin/FinishOncePlugin.kt
@@ -0,0 +1,31 @@
+package ksm.finish.once.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.plugin.Plugin
+
+public object FinishOncePlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + FinishOnceEntry()
+ }
+ }
+
+ public fun checkCanCreate(context: StateContext) {
+ context.require(FinishOnceEntry).checkCanCreate()
+ }
+
+ public fun finish(context: StateContext) {
+ context.require(FinishOnceEntry).finish()
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/lifecycle/LifecycleInterceptor.kt b/core/src/commonMain/kotlin/ksm/lifecycle/LifecycleInterceptor.kt
new file mode 100644
index 0000000..0258caf
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/lifecycle/LifecycleInterceptor.kt
@@ -0,0 +1,10 @@
+package ksm.lifecycle
+
+import ksm.context.StateContext
+
+public interface LifecycleInterceptor {
+ public fun onCreate(context: StateContext) {}
+ public fun onResume(context: StateContext) {}
+ public fun onPause(context: StateContext) {}
+ public fun onFinish(context: StateContext) {}
+}
diff --git a/core/src/commonMain/kotlin/ksm/lifecycle/StateContext.kt b/core/src/commonMain/kotlin/ksm/lifecycle/StateContext.kt
new file mode 100644
index 0000000..dff3a3c
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/lifecycle/StateContext.kt
@@ -0,0 +1,12 @@
+package ksm.lifecycle
+
+import ksm.context.StateContext
+import ksm.lifecycle.plugin.LifecyclePlugin
+import ksm.plugin.plugin
+
+public fun StateContext.addLifecycleInterceptor(interceptor: LifecycleInterceptor) {
+ plugin(LifecyclePlugin).addLifecycleInterceptor(
+ context = this,
+ observer = interceptor
+ )
+}
diff --git a/core/src/commonMain/kotlin/ksm/lifecycle/plugin/LifecycleEntry.kt b/core/src/commonMain/kotlin/ksm/lifecycle/plugin/LifecycleEntry.kt
new file mode 100644
index 0000000..72b65c0
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/lifecycle/plugin/LifecycleEntry.kt
@@ -0,0 +1,37 @@
+package ksm.lifecycle.plugin
+
+import ksm.context.StateContext
+import ksm.lifecycle.LifecycleInterceptor
+
+internal class LifecycleEntry : StateContext.Element {
+ override val key = LifecycleEntry
+
+ private val observers = mutableListOf()
+
+ fun addInterceptor(observer: LifecycleInterceptor) {
+ observers += observer
+ }
+
+ fun onCreate(context: StateContext) {
+ for (observer in observers) {
+ observer.onCreate(context)
+ }
+ }
+ fun onResume(context: StateContext) {
+ for (observer in observers) {
+ observer.onResume(context)
+ }
+ }
+ fun onPause(context: StateContext) {
+ for (observer in observers) {
+ observer.onPause(context)
+ }
+ }
+ fun onFinish(context: StateContext) {
+ for (observer in observers) {
+ observer.onFinish(context)
+ }
+ }
+
+ companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/lifecycle/plugin/LifecyclePlugin.kt b/core/src/commonMain/kotlin/ksm/lifecycle/plugin/LifecyclePlugin.kt
new file mode 100644
index 0000000..be88427
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/lifecycle/plugin/LifecyclePlugin.kt
@@ -0,0 +1,44 @@
+package ksm.lifecycle.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.plugin.Plugin
+
+public object LifecyclePlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + LifecycleEntry()
+ }
+ }
+
+ public fun addLifecycleInterceptor(
+ context: StateContext,
+ observer: LifecycleInterceptor
+ ) {
+ context.require(LifecycleEntry).addInterceptor(observer)
+ }
+
+ public fun onCreate(context: StateContext) {
+ context.require(LifecycleEntry).onCreate(context)
+ }
+ public fun onResume(context: StateContext) {
+ context.require(LifecycleEntry).onResume(context)
+ }
+ public fun onPause(context: StateContext) {
+ context.require(LifecycleEntry).onPause(context)
+ }
+ public fun onFinish(context: StateContext) {
+ context.require(LifecycleEntry).onFinish(context)
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/plugin/Plugin.kt b/core/src/commonMain/kotlin/ksm/plugin/Plugin.kt
new file mode 100644
index 0000000..ce6e668
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/plugin/Plugin.kt
@@ -0,0 +1,15 @@
+package ksm.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+
+public interface Plugin : StateContext.Element {
+ override val key: StateContext.Key<*>
+
+ @MutateContext
+ public fun install(context: StateContext): StateContext
+
+ public interface Singleton> : Plugin, StateContext.Element.Singleton {
+ override val key: StateContext.Key get() = this
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/plugin/StateController.kt b/core/src/commonMain/kotlin/ksm/plugin/StateController.kt
new file mode 100644
index 0000000..0d4c9e4
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/plugin/StateController.kt
@@ -0,0 +1,31 @@
+package ksm.plugin
+
+import ksm.context.StateContext
+
+@Deprecated(
+ message = "Use array access syntax instead",
+ replaceWith = ReplaceWith(
+ expression = "this[key]"
+ )
+)
+public inline fun StateContext.pluginOrNull(key: StateContext.Key): T? {
+ return this[key]
+}
+
+public inline fun StateContext.ifPlugin(
+ key: StateContext.Key,
+ block: T.() -> Unit
+) {
+ this[key]?.run(block)
+}
+
+public inline fun StateContext.plugin(key: StateContext.Key): T {
+ return this[key] ?: error("Plugin `${T::class.simpleName}` is not installed")
+}
+
+public inline fun StateContext.withPlugin(
+ key: StateContext.Key,
+ block: T.() -> Unit
+) {
+ plugin(key).run(block)
+}
diff --git a/core/src/commonMain/kotlin/ksm/serialization/BaseSerializationFormat.kt b/core/src/commonMain/kotlin/ksm/serialization/BaseSerializationFormat.kt
new file mode 100644
index 0000000..d81fad5
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/serialization/BaseSerializationFormat.kt
@@ -0,0 +1,9 @@
+package ksm.serialization
+
+import ksm.typed.TypedValue
+import kotlin.reflect.KType
+
+public interface BaseSerializationFormat {
+ public fun encode(value: TypedValue<*>)
+ public fun decode(type: KType): Any?
+}
diff --git a/core/src/commonMain/kotlin/ksm/serialization/BaseSerializationStore.kt b/core/src/commonMain/kotlin/ksm/serialization/BaseSerializationStore.kt
new file mode 100644
index 0000000..29a2bb2
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/serialization/BaseSerializationStore.kt
@@ -0,0 +1,16 @@
+package ksm.serialization
+
+import ksm.context.StateContext
+
+public interface BaseSerializationStore {
+ public suspend fun await() {}
+
+ public interface String : BaseSerializationStore, StateContext.Element {
+ override val key: Companion get() = String
+
+ public fun get(): kotlin.String?
+ public fun apply(string: kotlin.String)
+
+ public companion object : StateContext.Key
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/serialization/StateContext.kt b/core/src/commonMain/kotlin/ksm/serialization/StateContext.kt
new file mode 100644
index 0000000..ea4202e
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/serialization/StateContext.kt
@@ -0,0 +1,9 @@
+package ksm.serialization
+
+import ksm.context.StateContext
+import ksm.plugin.plugin
+import ksm.serialization.plugin.BaseSerializationPlugin
+
+public fun StateContext.restore() {
+ plugin(BaseSerializationPlugin).restore(context = this)
+}
diff --git a/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationParametersInterceptor.kt b/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationParametersInterceptor.kt
new file mode 100644
index 0000000..8ade552
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationParametersInterceptor.kt
@@ -0,0 +1,24 @@
+package ksm.serialization.plugin
+
+import ksm.state.parameters.interceptor.StateParametersInterceptor
+import ksm.typed.TypedValue
+
+internal class BaseSerializationParametersInterceptor : StateParametersInterceptor {
+ private val map = mutableMapOf>()
+
+ override fun onPut(
+ key: String,
+ value: TypedValue<*>
+ ) {
+ map[key] = TypedValue.Generic.of(value)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onReceive(key: String): TypedValue.Generic? {
+ return map[key] as TypedValue.Generic?
+ }
+
+ fun toMap(): Map> {
+ return map
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationPlugin.kt b/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationPlugin.kt
new file mode 100644
index 0000000..c2a2e63
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationPlugin.kt
@@ -0,0 +1,48 @@
+package ksm.serialization.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.lifecycle.addLifecycleInterceptor
+import ksm.plugin.Plugin
+import ksm.serialization.BaseSerializationFormat
+import ksm.state.parameters.interceptor.addParametersInterceptor
+
+public class BaseSerializationPlugin(format: BaseSerializationFormat) : Plugin {
+ override val key: Companion = BaseSerializationPlugin
+ private val controller = BaseSerializationStateController(format)
+
+ @MutateContext
+ override fun install(
+ context: StateContext
+ ): StateContext {
+ context.addConfigurationInterceptor(Configuration())
+ return context
+ }
+
+ private inner class Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ val parametersInterceptor = BaseSerializationParametersInterceptor()
+ context.addParametersInterceptor(parametersInterceptor)
+ context.addLifecycleInterceptor(Lifecycle(parametersInterceptor))
+ return context
+ }
+ }
+
+ private inner class Lifecycle(
+ val interceptor: BaseSerializationParametersInterceptor
+ ) : LifecycleInterceptor {
+ override fun onResume(context: StateContext) {
+ controller.commit(context, interceptor)
+ }
+ }
+
+ public fun restore(context: StateContext) {
+ controller.restore(context)
+ }
+
+ public companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationStateController.kt b/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationStateController.kt
new file mode 100644
index 0000000..371f1ac
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/serialization/plugin/BaseSerializationStateController.kt
@@ -0,0 +1,75 @@
+package ksm.serialization.plugin
+
+import ksm.context.StateContext
+import ksm.context.createChildContext
+import ksm.serialization.BaseSerializationFormat
+import ksm.stack.previousContextOrNull
+import ksm.state.name.setStateName
+import ksm.state.name.stateName
+import ksm.state.parameters.setStateParameter
+import ksm.typed.TypedValue
+import kotlin.reflect.typeOf
+
+private typealias SerializedStackType = List>>>
+
+internal class BaseSerializationStateController(
+ private val format: BaseSerializationFormat
+) : StateContext.Element {
+ override val key = BaseSerializationStateController
+
+ private val serializedStackType = typeOf()
+
+ @Suppress("UNCHECKED_CAST")
+ fun restore(root: StateContext) {
+ val serializedStack = format.decode(serializedStackType) as SerializedStackType
+ var current = root
+ for (serializedEntry in serializedStack) {
+ current = decodeEntry(current, serializedEntry.data)
+ }
+ }
+
+ private fun decodeEntry(
+ context: StateContext,
+ parameters: Map>
+ ): StateContext = context.createChildContext { child ->
+ val name = parameters[STATE_NAME_KEY]?.data as String?
+ if (name != null) {
+ child.setStateName(name)
+ }
+ for ((key, value) in parameters) {
+ if (key == STATE_NAME_KEY) continue
+ child.setStateParameter(key, value)
+ }
+ }
+
+ fun commit(context: StateContext, entry: BaseSerializationParametersInterceptor) {
+ var current: StateContext? = context
+ val serializedStack = mutableListOf>()
+
+ while (current != null) {
+ val encodedEntry = encodeEntry(current, entry)
+ serializedStack.add(
+ // Make sure root context is always on top
+ index = 0,
+ element = encodedEntry
+ )
+ current = context.previousContextOrNull
+ }
+
+ val value = TypedValue.of(serializedStack.toList())
+ format.encode(value)
+ }
+
+ private fun encodeEntry(
+ context: StateContext,
+ entry: BaseSerializationParametersInterceptor
+ ): TypedValue<*> {
+ val name = STATE_NAME_KEY to TypedValue.Generic.of(context.stateName)
+ val map = entry.toMap() + name
+ return TypedValue.of(map)
+ }
+
+ companion object : StateContext.Key {
+ const val STATE_NAME_KEY = "ksm.serialization.plugin.STATE_NAME"
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/stack/StateContext.kt b/core/src/commonMain/kotlin/ksm/stack/StateContext.kt
new file mode 100644
index 0000000..8f70257
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/stack/StateContext.kt
@@ -0,0 +1,35 @@
+package ksm.stack
+
+import ksm.context.StateContext
+import ksm.plugin.ifPlugin
+import ksm.serialization.plugin.BaseSerializationPlugin
+import ksm.stack.plugin.StateStackPlugin
+
+public val StateContext.previousContext: StateContext
+ get() = previousContextOrNull ?: error("Cannot get previous context")
+
+public val StateContext.previousContextOrNull: StateContext?
+ get() = this[StateStackPlugin]?.previousContextOrNull(context = this)
+
+public val StateContext.nextContext: StateContext
+ get() = nextContextOrNull ?: error("Cannot get next context")
+
+public val StateContext.nextContextOrNull: StateContext?
+ get() = this[StateStackPlugin]?.nextContextOrNull(context = this)
+
+public val StateContext.hasNextContext: Boolean
+ get() = nextContextOrNull != null
+
+public val StateContext.lastContext: StateContext
+ get() = lastContextOrNull ?: error("There is no child context")
+
+public val StateContext.lastContextOrNull: StateContext? get() {
+ var context: StateContext? = null
+ while (hasNextContext) {
+ context = nextContext
+ }
+ return context
+}
+
+public val StateContext.lastContextOrThis: StateContext
+ get() = lastContextOrNull ?: this
diff --git a/core/src/commonMain/kotlin/ksm/stack/StateController.kt b/core/src/commonMain/kotlin/ksm/stack/StateController.kt
new file mode 100644
index 0000000..ca95c53
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/stack/StateController.kt
@@ -0,0 +1,7 @@
+package ksm.stack
+
+import ksm.StateController
+
+public val StateController.previous: StateController get() {
+ return StateController(context.previousContext)
+}
diff --git a/core/src/commonMain/kotlin/ksm/stack/plugin/StateStackEntry.kt b/core/src/commonMain/kotlin/ksm/stack/plugin/StateStackEntry.kt
new file mode 100644
index 0000000..f517e52
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/stack/plugin/StateStackEntry.kt
@@ -0,0 +1,30 @@
+package ksm.stack.plugin
+
+import ksm.context.StateContext
+import ksm.context.finish
+import ksm.context.get
+
+internal class StateStackEntry(
+ private val previousEntry: StateStackEntry?
+) : StateContext.Element {
+ override val key = StateStackEntry
+
+ val previousContext: StateContext? get() = previousEntry?.context
+
+ private lateinit var context: StateContext
+
+ var nextContext: StateContext? = null
+ private set
+
+ fun attachContext(context: StateContext) {
+ this.context = context
+ previousEntry?.nextContext = context
+ }
+
+ fun onFinish() {
+ previousEntry?.nextContext = null
+ nextContext?.finish()
+ }
+
+ companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/stack/plugin/StateStackPlugin.kt b/core/src/commonMain/kotlin/ksm/stack/plugin/StateStackPlugin.kt
new file mode 100644
index 0000000..5f61d76
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/stack/plugin/StateStackPlugin.kt
@@ -0,0 +1,46 @@
+package ksm.stack.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.plugin.Plugin
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.lifecycle.addLifecycleInterceptor
+
+public object StateStackPlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ val entry = StateStackEntry(context[StateStackEntry])
+ context.addLifecycleInterceptor(Lifecycle(entry))
+ return context + entry
+ }
+ }
+
+ private class Lifecycle(val entry: StateStackEntry) : LifecycleInterceptor {
+ override fun onCreate(context: StateContext) {
+ entry.attachContext(context)
+ }
+ override fun onFinish(context: StateContext) {
+ entry.onFinish()
+ }
+ }
+
+ public fun previousContextOrNull(context: StateContext): StateContext? {
+ return context.require(StateStackEntry).previousContext
+ }
+
+ public fun nextContextOrNull(
+ context: StateContext
+ ): StateContext? {
+ return context.require(StateStackEntry).nextContext
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/LaunchState.kt b/core/src/commonMain/kotlin/ksm/state/LaunchState.kt
new file mode 100644
index 0000000..0ec4560
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/LaunchState.kt
@@ -0,0 +1,40 @@
+package ksm.state
+
+import ksm.StateController
+import ksm.context.StateContext
+import ksm.context.createChildContext
+import ksm.state.data.setStateData
+import ksm.state.name.setStateName
+import ksm.typed.TypedValue
+
+public inline fun StateController.launch(
+ name: String,
+ data: T
+) {
+ launch(name, TypedValue.of(data))
+}
+
+public fun StateController.launch(
+ name: String,
+ data: TypedValue<*> = TypedValue.of(Unit)
+) {
+ context.launchChildContext(name, data)
+}
+
+public inline fun StateContext.launchChildContext(
+ name: String,
+ data: T
+) {
+ launchChildContext(name, TypedValue.of(data))
+}
+
+public fun StateContext.launchChildContext(
+ name: String,
+ data: TypedValue<*> = TypedValue.of(Unit)
+) {
+ createChildContext { child ->
+ child.setStateName(name)
+ child.setStateData(data)
+ }
+}
+
diff --git a/core/src/commonMain/kotlin/ksm/state/StateScope.kt b/core/src/commonMain/kotlin/ksm/state/StateScope.kt
new file mode 100644
index 0000000..0425b9b
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/StateScope.kt
@@ -0,0 +1,7 @@
+package ksm.state
+
+import ksm.StateController
+import ksm.annotation.StateBuilderDSL
+
+@StateBuilderDSL
+public class StateScope(public val controller: StateController)
diff --git a/core/src/commonMain/kotlin/ksm/state/builder/StateBuilderScope.kt b/core/src/commonMain/kotlin/ksm/state/builder/StateBuilderScope.kt
new file mode 100644
index 0000000..9283049
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/builder/StateBuilderScope.kt
@@ -0,0 +1,7 @@
+package ksm.state.builder
+
+import ksm.annotation.StateBuilderDSL
+import ksm.context.StateContext
+
+@StateBuilderDSL
+public class StateBuilderScope(public val context: StateContext)
diff --git a/core/src/commonMain/kotlin/ksm/state/builder/StateControllerBuilder.kt b/core/src/commonMain/kotlin/ksm/state/builder/StateControllerBuilder.kt
new file mode 100644
index 0000000..ef12462
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/builder/StateControllerBuilder.kt
@@ -0,0 +1,19 @@
+package ksm.state.builder
+
+import ksm.builder.StateControllerBuilder
+import ksm.context.StateContext
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.lifecycle.addLifecycleInterceptor
+
+public fun StateControllerBuilder.states(block: StateRouteScope.() -> Unit) {
+ val interceptor = object : LifecycleInterceptor {
+ override fun onCreate(context: StateContext) {
+ val scope = StateRouteScope(context)
+ block(scope)
+ if (!scope.intercepted) {
+ error("Cannot launch state because there is no handlers for this")
+ }
+ }
+ }
+ context.addLifecycleInterceptor(interceptor)
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/builder/StateRouteScope.kt b/core/src/commonMain/kotlin/ksm/state/builder/StateRouteScope.kt
new file mode 100644
index 0000000..416eeb7
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/builder/StateRouteScope.kt
@@ -0,0 +1,26 @@
+package ksm.state.builder
+
+import ksm.annotation.StateBuilderDSL
+import ksm.context.StateContext
+
+@StateBuilderDSL
+public class StateRouteScope(
+ public val context: StateContext
+) {
+ internal var intercepted = false
+
+ public fun intercept() {
+ require(!intercepted) { "State was already intercepted" }
+ intercepted = true
+ }
+
+ public inline fun intercept(
+ predicate: () -> Boolean,
+ block: StateBuilderScope.() -> Unit
+ ) {
+ if (predicate()) {
+ intercept()
+ block(StateBuilderScope(context))
+ }
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/data/StateContext.kt b/core/src/commonMain/kotlin/ksm/state/data/StateContext.kt
new file mode 100644
index 0000000..432cbc1
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/data/StateContext.kt
@@ -0,0 +1,25 @@
+package ksm.state.data
+
+import ksm.context.StateContext
+import ksm.state.parameters.receiveStateParameter
+import ksm.state.parameters.setStateParameter
+import ksm.typed.TypedValue
+import kotlin.reflect.KType
+
+public const val STATE_DATA_KEY: String = "ksm.state.data.STATE_DATA"
+
+public inline fun StateContext.setStateData(value: T) {
+ setStateData(TypedValue.of(value))
+}
+
+public fun StateContext.setStateData(value: TypedValue<*>) {
+ setStateParameter(STATE_DATA_KEY, value)
+}
+
+public inline fun StateContext.receive(): T {
+ return receiveStateParameter(STATE_DATA_KEY) ?: error("Data is not provided")
+}
+
+public fun StateContext.receive(type: KType): Any? {
+ return receiveStateParameter(STATE_DATA_KEY, type)
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/data/StateController.kt b/core/src/commonMain/kotlin/ksm/state/data/StateController.kt
new file mode 100644
index 0000000..f117a75
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/data/StateController.kt
@@ -0,0 +1,7 @@
+package ksm.state.data
+
+import ksm.StateController
+
+public inline fun StateController.receive(): T {
+ return context.receive()
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/name/StateContext.kt b/core/src/commonMain/kotlin/ksm/state/name/StateContext.kt
new file mode 100644
index 0000000..91c9075
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/name/StateContext.kt
@@ -0,0 +1,20 @@
+package ksm.state.name
+
+import ksm.context.StateContext
+import ksm.plugin.plugin
+import ksm.state.name.plugin.StateNamePlugin
+
+public fun StateContext.setStateName(name: String) {
+ plugin(StateNamePlugin).setName(
+ context = this,
+ name = name
+ )
+}
+
+public val StateContext.stateName: String get() {
+ return stateNameOrNull ?: error("Current state has no name")
+}
+
+public val StateContext.stateNameOrNull: String? get() {
+ return plugin(StateNamePlugin).stateName(context = this)
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/name/StateRouteScope.kt b/core/src/commonMain/kotlin/ksm/state/name/StateRouteScope.kt
new file mode 100644
index 0000000..5c170d7
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/name/StateRouteScope.kt
@@ -0,0 +1,14 @@
+package ksm.state.name
+
+import ksm.state.builder.StateBuilderScope
+import ksm.state.builder.StateRouteScope
+
+public inline fun StateRouteScope.named(
+ string: String,
+ block: StateBuilderScope.() -> Unit
+) {
+ intercept(
+ predicate = { context.stateNameOrNull == string },
+ block = block
+ )
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/name/plugin/StateNameEntry.kt b/core/src/commonMain/kotlin/ksm/state/name/plugin/StateNameEntry.kt
new file mode 100644
index 0000000..8c389cc
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/name/plugin/StateNameEntry.kt
@@ -0,0 +1,11 @@
+package ksm.state.name.plugin
+
+import ksm.context.StateContext
+
+internal class StateNameEntry : StateContext.Element {
+ override val key = StateNameEntry
+
+ var name: String? = null
+
+ companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/name/plugin/StateNamePlugin.kt b/core/src/commonMain/kotlin/ksm/state/name/plugin/StateNamePlugin.kt
new file mode 100644
index 0000000..728413f
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/name/plugin/StateNamePlugin.kt
@@ -0,0 +1,34 @@
+package ksm.state.name.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.plugin.Plugin
+
+public object StateNamePlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + StateNameEntry()
+ }
+ }
+
+ public fun setName(
+ context: StateContext,
+ name: String
+ ) {
+ context.require(StateNameEntry).name = name
+ }
+
+ public fun stateName(context: StateContext): String? {
+ return context.require(StateNameEntry).name
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/parameters/StateContext.kt b/core/src/commonMain/kotlin/ksm/state/parameters/StateContext.kt
new file mode 100644
index 0000000..60dcb7e
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/parameters/StateContext.kt
@@ -0,0 +1,33 @@
+package ksm.state.parameters
+
+import ksm.context.StateContext
+import ksm.plugin.plugin
+import ksm.state.parameters.plugin.StateParametersPlugin
+import ksm.typed.TypedValue
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
+
+public fun StateContext.setStateParameter(
+ key: String,
+ value: TypedValue
+) {
+ plugin(StateParametersPlugin).put(
+ context = this,
+ key = key,
+ value = value
+ )
+}
+
+public inline fun StateContext.receiveStateParameter(key: String): T? {
+ return receiveStateParameter(key, typeOf())
+}
+
+public fun StateContext.receiveStateParameter(
+ key: String,
+ type: KType
+): T? {
+ return plugin(StateParametersPlugin).receive(
+ context = this,
+ key = key
+ )?.get(type)
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/StateContext.kt b/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/StateContext.kt
new file mode 100644
index 0000000..9e30c22
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/StateContext.kt
@@ -0,0 +1,34 @@
+package ksm.state.parameters.interceptor
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.plugin.plugin
+import ksm.state.parameters.plugin.StateParametersPlugin
+import ksm.typed.TypedValue
+
+@MutateContext
+public fun StateContext.addParametersInterceptor(
+ onPut: (key: String, value: TypedValue<*>) -> Unit = { _, _ -> },
+ onReceive: (String) -> TypedValue.Generic<*>? = { null }
+) {
+ val interceptor = object : StateParametersInterceptor {
+ override fun onPut(key: String, value: TypedValue<*>) {
+ onPut(key, value)
+ }
+ override fun onReceive(key: String): TypedValue.Generic? {
+ @Suppress("UNCHECKED_CAST")
+ return onReceive(key) as TypedValue.Generic?
+ }
+ }
+ addParametersInterceptor(interceptor)
+}
+
+@MutateContext
+public fun StateContext.addParametersInterceptor(
+ interceptor: StateParametersInterceptor
+) {
+ plugin(StateParametersPlugin).addParametersInterceptor(
+ context = this,
+ interceptor = interceptor
+ )
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/StateParametersInterceptor.kt b/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/StateParametersInterceptor.kt
new file mode 100644
index 0000000..d5df1e9
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/StateParametersInterceptor.kt
@@ -0,0 +1,8 @@
+package ksm.state.parameters.interceptor
+
+import ksm.typed.TypedValue
+
+public interface StateParametersInterceptor {
+ public fun onPut(key: String, value: TypedValue<*>) {}
+ public fun onReceive(key: String): TypedValue.Generic? = null
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/memory/MemoryStateParametersInterceptor.kt b/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/memory/MemoryStateParametersInterceptor.kt
new file mode 100644
index 0000000..0936ec5
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/parameters/interceptor/memory/MemoryStateParametersInterceptor.kt
@@ -0,0 +1,17 @@
+package ksm.state.parameters.interceptor.memory
+
+import ksm.state.parameters.interceptor.StateParametersInterceptor
+import ksm.typed.TypedValue
+
+internal class MemoryStateParametersInterceptor : StateParametersInterceptor {
+ private val map = mutableMapOf>()
+
+ override fun onPut(key: String, value: TypedValue<*>) {
+ map[key] = TypedValue.Generic.of(value)
+ }
+
+ @Suppress("UNCHECKED_CAST")
+ override fun onReceive(key: String): TypedValue.Generic? {
+ return map[key] as TypedValue.Generic?
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/parameters/plugin/StateParametersEntry.kt b/core/src/commonMain/kotlin/ksm/state/parameters/plugin/StateParametersEntry.kt
new file mode 100644
index 0000000..17be924
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/parameters/plugin/StateParametersEntry.kt
@@ -0,0 +1,34 @@
+package ksm.state.parameters.plugin
+
+import ksm.context.StateContext
+import ksm.state.parameters.interceptor.StateParametersInterceptor
+import ksm.typed.TypedValue
+
+internal class StateParametersEntry : StateContext.Element {
+ override val key = StateParametersEntry
+
+ private val interceptors = mutableListOf()
+
+ fun addInterceptor(interceptor: StateParametersInterceptor) {
+ interceptors += interceptor
+ }
+
+ fun onPut(
+ key: String,
+ value: TypedValue<*>
+ ) {
+ for (interceptor in interceptors) {
+ interceptor.onPut(key, value)
+ }
+ }
+
+ fun onReceive(key: String): TypedValue.Generic? {
+ for (interceptor in interceptors.asReversed()) {
+ val value = interceptor.onReceive(key)
+ if (value != null) return value
+ }
+ return null
+ }
+
+ companion object : StateContext.Key
+}
diff --git a/core/src/commonMain/kotlin/ksm/state/parameters/plugin/StateParametersPlugin.kt b/core/src/commonMain/kotlin/ksm/state/parameters/plugin/StateParametersPlugin.kt
new file mode 100644
index 0000000..d62d139
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/state/parameters/plugin/StateParametersPlugin.kt
@@ -0,0 +1,52 @@
+package ksm.state.parameters.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.plugin.Plugin
+import ksm.state.parameters.interceptor.StateParametersInterceptor
+import ksm.state.parameters.interceptor.memory.MemoryStateParametersInterceptor
+import ksm.typed.TypedValue
+
+public object StateParametersPlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(
+ context: StateContext
+ ): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ val entry = StateParametersEntry()
+ entry.addInterceptor(MemoryStateParametersInterceptor())
+ return context + entry
+ }
+ }
+
+ public fun put(
+ context: StateContext,
+ key: String,
+ value: TypedValue
+ ) {
+ context.require(StateParametersEntry).onPut(key, value)
+ }
+
+ public fun receive(
+ context: StateContext,
+ key: String
+ ): TypedValue.Generic? {
+ return context.require(StateParametersEntry).onReceive(key)
+ }
+
+ public fun addParametersInterceptor(
+ context: StateContext,
+ interceptor: StateParametersInterceptor
+ ) {
+ context.require(StateParametersEntry).addInterceptor(interceptor)
+ }
+}
diff --git a/core/src/commonMain/kotlin/ksm/typed/TypedValue.kt b/core/src/commonMain/kotlin/ksm/typed/TypedValue.kt
new file mode 100644
index 0000000..151b7b2
--- /dev/null
+++ b/core/src/commonMain/kotlin/ksm/typed/TypedValue.kt
@@ -0,0 +1,32 @@
+package ksm.typed
+
+import kotlin.reflect.KType
+import kotlin.reflect.typeOf
+
+public class TypedValue(
+ public val data: T,
+ public val type: KType
+) {
+ public fun interface Generic {
+ public fun get(type: KType): T
+
+ public companion object {
+ public inline fun of(value: T): Generic {
+ return of(TypedValue.of(value))
+ }
+
+ public fun of(value: TypedValue): Generic {
+ return Generic { type ->
+ require(type == value.type)
+ value.data
+ }
+ }
+ }
+ }
+
+ public companion object {
+ public inline fun of(value: T): TypedValue {
+ return TypedValue(value, typeOf())
+ }
+ }
+}
diff --git a/docs/EXPOSING_API.md b/docs/EXPOSING_API.md
new file mode 100644
index 0000000..5a47e87
--- /dev/null
+++ b/docs/EXPOSING_API.md
@@ -0,0 +1,42 @@
+# Exposing Plugin API
+
+Class or object implementing `Plugin` interface must have all public methods
+required to interact with the feature. A good example of such plugin is
+`CoroutinesPlugin`:
+
+```kotlin
+object CoroutinesPlugin : Plugin {
+ override fun install(context: StateContext): StateContext { ... }
+
+ fun coroutineContext(context: StateContext): CoroutineContext {
+ return context.require(CoroutineContextKey).coroutineContext
+ }
+}
+```
+
+All the keys of the plugin should be internal or even private, you should not share
+keys directly but rather provide convenient extensions on top of StateContext.
+
+Continuing with `CoroutinesPlugin`, the extension to get coroutineContext looks like
+this:
+
+```kotlin
+val StateContext.coroutineContext: CoroutineContext get() {
+ return plugin(CoroutinesPlugin).coroutineContext(context = this)
+}
+```
+
+Here you can see why you should not share Key:
+
+If there is no plugin
+installed the Key will be absent and any operation will fail saying
+'Cannot find element with key $key'. And the name of the key can
+be literally anything and user of the plugin probably knows
+no more than the plugin name.
+
+When you get plugin using `plugin` function, it first checks if the
+plugin itself is installed. And if it isn't, it throws an exception
+saying 'Plugin $name is not installed' which is ultimately more clear.
+
+Additionally, that way we know that when 'Cannot find element with key $key'
+exception in thrown, it's due to the bug in the plugin and not user's fault.
diff --git a/docs/PHASES.md b/docs/PHASES.md
new file mode 100644
index 0000000..30ba752
--- /dev/null
+++ b/docs/PHASES.md
@@ -0,0 +1,198 @@
+# Phases
+
+KSM is based on different callbacks and interceptors and therefore,
+there are some phases you should be aware about.
+
+Note: There is no `Phase` in the library as an interface or something
+similar, it's just a good mental model.
+
+## Visual Scheme
+
+```mermaid
+stateDiagram-v2
+ [*] --> StateController
+ StateController --> Plugins
+ Plugins: Apply Plugins
+ state Plugins {
+ [*] --> install
+ install: // Installing interceptors
Plugin.install(context#58; StateContext)#58; StateContext
+ install --> [*]
+ }
+ Plugins --> RootStateController
+ RootStateController: Root StateController is configured
+ RootStateController --> ChildStateContext
+ ChildStateContext: Create Child State
+ state ChildStateContext {
+ [*] --> onConfigure
+ onConfigure: onConfigure
+ onConfigure --> onConfigureDescription
+ onConfigureDescription: // Registering entries
StateContextInterceptor.onConfigure(context)#58; Context
+ onConfigureDescription --> fillEntries
+ fillEntries: Entries set up#59; usually done by extension functions
+ fillEntries --> onCreate
+ onCreate --> onCreateDescription
+ onCreateDescription: // Plugins can handle filled data
StateContextInterceptor.onCreate(childContext)
+ onCreateDescription --> onResume
+ onResume --> onResumeDescription
+ onResumeDescription: // Invoked right after onCreate or after child finish
StateContextInterceptor.onResume(childContext)
+ onResumeDescription --> stateLives
+ stateLives: -- Child state lives --
+ stateLives --> onFinish
+ onFinish --> onFinishDescription
+ onFinish --> onResume
+ onFinishDescription: // Invoked after StateContext.finish() invoked
StateContextInterceptor.onFinish(childContext)
+ onFinishDescription --> [*]
+ }
+ ChildStateContext --> [*]
+```
+
+## Plugin Installation
+
+First phase is a plugin installation. This is where a life of the
+root `StateController` begins. This is where all plugins install
+their interceptors and store configuration:
+
+```kotlin
+class CoroutinesPlugin(
+ // Configuration is just plugin properties
+ val context: CoroutineContext
+) : Plugin {
+
+ override fun install(context: StateContext) {
+ return context + StateContextInterceptor(context)
+ }
+}
+```
+
+Plugins can be installed using `createRawStateController` function.
+It is called 'Raw' because there is no plugins except builtins and
+the end user probably should not use this function. The function
+is used by creators of libraries that integrates ksm into some ecosystems.
+For example:
+
+```kotlin
+fun createTelegramKSM(): StateController {
+ return createRawStateController {
+ install(TelegramKSM)
+ }
+}
+```
+
+### Restrictions
+
+- **Do not read properties from `context`, only write them**
+
+ Current context is incomplete and will be modified at
+ later point of time by another plugins, so you should
+ not save it or read any properties from it
+
+## State Phases
+
+The following phases relate to creation of child states, and they
+are being invoked at `StateContext.createChildContext`. And
+all the following methods are methods of `StateContextInterceptor`
+
+### onConfigure
+
+When creating a new `StateContext`, the first method that is being
+invoked is `onConfigure`. Let's look at the signature:
+
+```kotlin
+fun onConfigure(context: StateContext): StateContext
+```
+
+It takes context and returns context and this is the place where
+you can register entries scoped to child context. Continuing with
+the example with coroutines:
+
+```kotlin
+class CoroutinesContextInterceptor(
+ val initialContext: CoroutineContext
+) : StateContextInterceptor {
+ override fun onConfigure(context: StateContext): StateContext {
+ val previousEntry = context[CoroutinesEntry]
+ // Support for structured concurrency
+ return context + CoroutinesEntry(previousEntry)
+ }
+}
+```
+
+#### Restrictions
+
+- **Do not set any interceptors**
+
+ There is a chance that interceptor that you try to set was already
+ invoked earlier by another plugin and if you set interceptor at that phase,
+ it will lead to inconsistent behaviour in different cases
+
+
+- **Do not read from context except for entries under plugin control**
+
+ Even though it is guaranteed that interceptors of plugins are called in
+ the order that plugins were registered, it's you should not access other
+ plugins in `onConfigure` method. _This is to be discussed, but there is
+ no seemed reason for this to be allowed._
+
+### onCreate
+
+Continuing with `StateContext.createChildContext`:
+
+```kotlin
+fun StateContext.createChildContext(apply: (StateContext) -> Unit) {
+ // All plugins register their entries
+ val childContext = this.onConfigure(applied)
+ // This is where all the setup of entries happens
+ apply(childContext)
+ childContext.onCreate()
+}
+```
+
+As we can clearly see from the current code snippet, onCreate is called
+when all context entries were set up. What does set up mean? Let's consider
+the following simplified example:
+
+We have `NavigationPlugin`, which registers `NavigationEntry` in
+`NavigationInterceptor` in `onConfigure` method, similarly to the
+previous example:
+
+```kotlin
+class NavigationInterceptor {
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + NavigationEntry()
+ }
+}
+```
+
+And the `NavigationEntry` itself looks like this:
+
+```kotlin
+class NavigationEntry {
+ var route: String? = null
+}
+```
+
+It is expected that `route` will be set in `apply` lambda of
+`createChildContext` function. And `onCreate` is a perfect place to receive it:
+
+```kotlin
+class NavigationInterceptor(
+ private val controller: NavController
+) {
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + NavigationEntry()
+ }
+
+ override fun onCreate(context: StateContext) {
+ val entry = context.require(NavigationEntry)
+ val route = entry.route ?: return
+ controller.navigate(route)
+ }
+}
+```
+
+### Other Lifecycle Events
+
+Also, there are other lifecycle events:
+
+- `StateContextInterceptor.onResume` – when context after this was finished
+- `StateContextInterceptor.onFinish` – when context was finished
diff --git a/docs/STRUCTURED_NAVIGATION.md b/docs/STRUCTURED_NAVIGATION.md
new file mode 100644
index 0000000..ef02718
--- /dev/null
+++ b/docs/STRUCTURED_NAVIGATION.md
@@ -0,0 +1,8 @@
+# Structured Navigation
+
+Structured navigation is like a structured concurrency when
+entries can have children and their children can have children.
+Finishing one entry will cause its child to finish. And
+starting new child from entry also finishes its previous child.
+
+
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 0000000..2d8d1e4
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1 @@
+android.useAndroidX=true
\ No newline at end of file
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
new file mode 100644
index 0000000..f13a039
--- /dev/null
+++ b/gradle/libs.versions.toml
@@ -0,0 +1,35 @@
+[versions]
+
+kotlin = "1.9.23"
+kotlinx-coroutines = "1.8.0"
+kotlinx-serialization = "1.6.3"
+android-gradle = "7.3.1"
+compose-runtime = "1.6.5"
+compose-compiler = "1.5.11"
+androidx-navigation = "2.7.7"
+androidx-lifecycle = "2.7.0"
+ktgbotapi = "11.0.0"
+mdi = "0.0.37"
+
+ksm = "0.0.1"
+
+[libraries]
+
+kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
+kotlinx-serialization = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" }
+
+compose-foundation = { module = "androidx.compose.foundation:foundation", version.ref = "compose-runtime" }
+compose-runtime = { module = "androidx.compose.runtime:runtime", version.ref = "compose-runtime" }
+compose-runtime-saveable = { module = "androidx.compose.runtime:runtime-saveable", version.ref = "compose-runtime" }
+androidx-navigation-ui = { module = "androidx.navigation:navigation-ui-ktx", version.ref = "androidx-navigation" }
+androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidx-navigation" }
+androidx-lifecycle = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" }
+
+ktgbotapi = { module = "dev.inmo:tgbotapi", version.ref = "ktgbotapi" }
+
+mdi = { module = "app.meetacy.di:core", version.ref = "mdi" }
+
+# gradle plugins
+kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
+kotlinx-serialization-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
+android-gradle-plugin = { module = "com.android.tools.build:gradle", version.ref = "android-gradle" }
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 0000000..c1962a7
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..37aef8d
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-bin.zip
+networkTimeout=10000
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 0000000..aeb74cb
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,245 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 0000000..93e3f59
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,92 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kotlinx-coroutines/build.gradle.kts b/kotlinx-coroutines/build.gradle.kts
new file mode 100644
index 0000000..dc0fcb7
--- /dev/null
+++ b/kotlinx-coroutines/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("kmp-library-convention")
+}
+
+version = libs.versions.ksm.get()
+
+dependencies {
+ commonMainApi(projects.core)
+ commonMainImplementation(libs.kotlinx.coroutines.core)
+}
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/StateContext.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/StateContext.kt
new file mode 100644
index 0000000..1c98f98
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/StateContext.kt
@@ -0,0 +1,29 @@
+package ksm.coroutines
+
+import kotlinx.coroutines.CoroutineScope
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.coroutines.interceptor.AwaitInterceptor
+import ksm.coroutines.plugin.AwaitPlugin
+import ksm.coroutines.plugin.CoroutinesPlugin
+import ksm.plugin.plugin
+import kotlin.coroutines.CoroutineContext
+
+public val StateContext.coroutineScope: CoroutineScope get() {
+ return CoroutineScope(coroutineContext)
+}
+
+public val StateContext.coroutineContext: CoroutineContext get() {
+ return plugin(CoroutinesPlugin).coroutineContext(context = this)
+}
+
+public fun StateContext.addAwaitInterceptor(interceptor: AwaitInterceptor) {
+ plugin(AwaitPlugin).addAwaitInterceptor(
+ context = this,
+ interceptor = interceptor
+ )
+}
+
+public suspend fun StateContext.await() {
+ plugin(AwaitPlugin).await(context = this)
+}
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/StateController.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/StateController.kt
new file mode 100644
index 0000000..37718f5
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/StateController.kt
@@ -0,0 +1,7 @@
+package ksm.coroutines
+
+import kotlinx.coroutines.CoroutineScope
+import ksm.StateController
+
+public val StateController.coroutineScope: CoroutineScope
+ get() = context.coroutineScope
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/interceptor/AwaitInterceptor.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/interceptor/AwaitInterceptor.kt
new file mode 100644
index 0000000..0dccd69
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/interceptor/AwaitInterceptor.kt
@@ -0,0 +1,5 @@
+package ksm.coroutines.interceptor
+
+public fun interface AwaitInterceptor {
+ public suspend fun await()
+}
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/AwaitEntry.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/AwaitEntry.kt
new file mode 100644
index 0000000..51dcc03
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/AwaitEntry.kt
@@ -0,0 +1,22 @@
+package ksm.coroutines.plugin
+
+import ksm.context.StateContext
+import ksm.coroutines.interceptor.AwaitInterceptor
+
+internal class AwaitEntry : StateContext.Element {
+ override val key = AwaitEntry
+
+ private val awaitInterceptors = mutableListOf()
+
+ suspend fun await() {
+ for (interceptor in awaitInterceptors) {
+ interceptor.await()
+ }
+ }
+
+ fun addAwaitInterceptor(interceptor: AwaitInterceptor) {
+ awaitInterceptors += interceptor
+ }
+
+ companion object : StateContext.Key
+}
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/AwaitPlugin.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/AwaitPlugin.kt
new file mode 100644
index 0000000..ebf7781
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/AwaitPlugin.kt
@@ -0,0 +1,35 @@
+package ksm.coroutines.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.coroutines.interceptor.AwaitInterceptor
+import ksm.plugin.Plugin
+
+public object AwaitPlugin : Plugin.Singleton {
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + AwaitEntry()
+ }
+ }
+
+ public suspend fun await(context: StateContext) {
+ context.require(AwaitEntry).await()
+ }
+
+ public fun addAwaitInterceptor(
+ context: StateContext,
+ interceptor: AwaitInterceptor
+ ) {
+ context.require(AwaitEntry).addAwaitInterceptor(interceptor)
+ }
+}
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/CoroutinesEntry.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/CoroutinesEntry.kt
new file mode 100644
index 0000000..a1927d3
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/CoroutinesEntry.kt
@@ -0,0 +1,33 @@
+package ksm.coroutines.plugin
+
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.newCoroutineContext
+import ksm.context.StateContext
+import ksm.coroutines.interceptor.AwaitInterceptor
+import kotlin.coroutines.CoroutineContext
+
+internal class CoroutinesEntry(
+ context: StateContext,
+ initialCoroutineContext: CoroutineContext
+) : StateContext.Element {
+ override val key = CoroutinesEntry
+
+ val coroutineContext: CoroutineContext
+
+ init {
+ val parentCoroutineContext = context[CoroutinesEntry]
+ ?.coroutineContext
+ ?: initialCoroutineContext
+
+ val parentJob = parentCoroutineContext[Job]
+ val job = Job(parentJob)
+
+ coroutineContext = CoroutineScope(parentCoroutineContext).newCoroutineContext(job)
+ }
+
+ fun cancel() = coroutineContext.cancel()
+
+ companion object : StateContext.Key
+}
diff --git a/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/CoroutinesPlugin.kt b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/CoroutinesPlugin.kt
new file mode 100644
index 0000000..961b4d6
--- /dev/null
+++ b/kotlinx-coroutines/src/commonMain/kotlin/ksm/coroutines/plugin/CoroutinesPlugin.kt
@@ -0,0 +1,43 @@
+package ksm.coroutines.plugin
+
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.plugin.Plugin
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.coroutines.interceptor.AwaitInterceptor
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.lifecycle.addLifecycleInterceptor
+import kotlin.coroutines.CoroutineContext
+import kotlin.coroutines.EmptyCoroutineContext
+
+public class CoroutinesPlugin(
+ private val coroutineContext: CoroutineContext
+) : Plugin {
+ override val key: Companion = CoroutinesPlugin
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration())
+ return context
+ }
+
+ private inner class Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ val entry = CoroutinesEntry(context, coroutineContext)
+ context.addLifecycleInterceptor(Lifecycle(entry))
+ return context + entry
+ }
+ }
+
+ private inner class Lifecycle(val entry: CoroutinesEntry) : LifecycleInterceptor {
+ override fun onFinish(context: StateContext) { entry.cancel() }
+ }
+
+ public fun coroutineContext(context: StateContext): CoroutineContext {
+ return context.require(CoroutinesEntry).coroutineContext
+ }
+
+ public companion object : StateContext.Key, Plugin by CoroutinesPlugin(EmptyCoroutineContext)
+}
diff --git a/kotlinx-serialization-json/build.gradle.kts b/kotlinx-serialization-json/build.gradle.kts
new file mode 100644
index 0000000..98b6457
--- /dev/null
+++ b/kotlinx-serialization-json/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id("kmp-library-convention")
+ kotlin("plugin.serialization")
+}
+
+version = libs.versions.ksm.get()
+
+dependencies {
+ commonMainApi(projects.core)
+ commonMainImplementation(libs.kotlinx.serialization)
+}
diff --git a/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/KotlinxSerializationFormat.kt b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/KotlinxSerializationFormat.kt
new file mode 100644
index 0000000..8c2a43a
--- /dev/null
+++ b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/KotlinxSerializationFormat.kt
@@ -0,0 +1,35 @@
+package ksm.kotlinx.serialization.plugin
+
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.serializer
+import ksm.kotlinx.serialization.plugin.serializer.GenericValueDeserializer
+import ksm.kotlinx.serialization.plugin.serializer.TypedValueSerializer
+import ksm.serialization.BaseSerializationFormat
+import ksm.serialization.BaseSerializationStore
+import ksm.typed.TypedValue
+import kotlin.reflect.KType
+
+internal class KotlinxSerializationFormat(
+ private val store: BaseSerializationStore.String,
+ json: Json
+) : BaseSerializationFormat {
+ private val json = Json(json) {
+ serializersModule = SerializersModule {
+ include(serializersModule)
+ contextual(TypedValue::class, TypedValueSerializer)
+ contextual(TypedValue.Generic::class, GenericValueDeserializer)
+ }
+ }
+
+ override fun encode(value: TypedValue<*>) {
+ val serializer = json.serializersModule.serializer(value.type)
+ store.apply(json.encodeToString(serializer, value))
+ }
+
+ override fun decode(type: KType): Any? {
+ val string = store.get() ?: return null
+ val deserializer = json.serializersModule.serializer(type)
+ return json.decodeFromString(deserializer, string)
+ }
+}
diff --git a/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/KotlinxSerializationPlugin.kt b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/KotlinxSerializationPlugin.kt
new file mode 100644
index 0000000..a4eaf61
--- /dev/null
+++ b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/KotlinxSerializationPlugin.kt
@@ -0,0 +1,28 @@
+package ksm.kotlinx.serialization.plugin
+
+import kotlinx.serialization.json.Json
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.install
+import ksm.plugin.Plugin
+import ksm.serialization.BaseSerializationStore
+import ksm.serialization.plugin.BaseSerializationPlugin
+
+public class KotlinxSerializationPlugin(
+ private val json: Json = Json,
+ private val store: BaseSerializationStore.String? = null
+) : Plugin {
+ override val key: Companion = KotlinxSerializationPlugin
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ val store = this.store
+ ?: context[BaseSerializationStore.String]
+ ?: error("Please either provide BaseSerializationStore using constructor or install the plugin that will provide it")
+ val format = KotlinxSerializationFormat(store, json)
+ val basePlugin = BaseSerializationPlugin(format)
+ return context.install(basePlugin)
+ }
+
+ public companion object : StateContext.Key
+}
diff --git a/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/serializer/GenericValueDeserializer.kt b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/serializer/GenericValueDeserializer.kt
new file mode 100644
index 0000000..fdf664d
--- /dev/null
+++ b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/serializer/GenericValueDeserializer.kt
@@ -0,0 +1,32 @@
+package ksm.kotlinx.serialization.plugin.serializer
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.serializer
+import ksm.typed.TypedValue
+
+public object GenericValueDeserializer : KSerializer> {
+ @OptIn(ExperimentalSerializationApi::class)
+ override val descriptor: SerialDescriptor = SerialDescriptor(
+ serialName = "TypedValueSerializer",
+ original = JsonElement.serializer().descriptor
+ )
+
+ override fun serialize(
+ encoder: Encoder,
+ value: TypedValue.Generic<*>
+ ) {
+ error("TypedValue.Generic cannot be serialized, only deserialized")
+ }
+
+ override fun deserialize(decoder: Decoder): TypedValue.Generic<*> {
+ return TypedValue.Generic { type ->
+ val deserializer = decoder.serializersModule.serializer(type)
+ deserializer.deserialize(decoder)
+ }
+ }
+}
diff --git a/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/serializer/TypedValueSerializer.kt b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/serializer/TypedValueSerializer.kt
new file mode 100644
index 0000000..42f3891
--- /dev/null
+++ b/kotlinx-serialization-json/src/commonMain/kotlin/ksm/kotlinx/serialization/plugin/serializer/TypedValueSerializer.kt
@@ -0,0 +1,31 @@
+package ksm.kotlinx.serialization.plugin.serializer
+
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerializationStrategy
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.serializer
+import ksm.typed.TypedValue
+
+public object TypedValueSerializer : KSerializer> {
+ @OptIn(ExperimentalSerializationApi::class)
+ override val descriptor: SerialDescriptor = SerialDescriptor(
+ serialName = "TypedValueSerializer",
+ original = JsonElement.serializer().descriptor
+ )
+
+ override fun serialize(
+ encoder: Encoder,
+ value: TypedValue<*>
+ ) {
+ val baseSerializer = encoder.serializersModule.serializer(value.type)
+ baseSerializer.serialize(encoder, value.data)
+ }
+
+ override fun deserialize(decoder: Decoder): TypedValue<*> {
+ error("This type cannot be deserialized, use TypedValue.Generic instead")
+ }
+}
diff --git a/ktgbotapi/build.gradle.kts b/ktgbotapi/build.gradle.kts
new file mode 100644
index 0000000..81ff887
--- /dev/null
+++ b/ktgbotapi/build.gradle.kts
@@ -0,0 +1,27 @@
+plugins {
+ kotlin("multiplatform")
+ id("publication-convention")
+}
+
+kotlin {
+ explicitApi()
+
+ jvmToolchain(17)
+
+ jvm()
+
+ js(IR) {
+ browser()
+ nodejs()
+ }
+}
+
+version = libs.versions.ksm.get()
+
+dependencies {
+ commonMainApi(projects.core)
+ commonMainApi(projects.kotlinxCoroutines)
+ commonMainApi(projects.kotlinxSerializationJson)
+ commonMainImplementation(libs.kotlinx.coroutines.core)
+ commonMainImplementation(libs.ktgbotapi)
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateBuilderScope.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateBuilderScope.kt
new file mode 100644
index 0000000..565ea6f
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateBuilderScope.kt
@@ -0,0 +1,8 @@
+package ksm.ktgbotapi
+
+import ksm.state.StateScope
+import ksm.state.builder.StateBuilderScope
+
+public fun StateBuilderScope.execute(block: suspend StateScope.() -> Unit) {
+ context.setExecuteBlock(block)
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateContext.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateContext.kt
new file mode 100644
index 0000000..7c2367f
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateContext.kt
@@ -0,0 +1,38 @@
+package ksm.ktgbotapi
+
+import dev.inmo.tgbotapi.bot.TelegramBot
+import dev.inmo.tgbotapi.types.message.abstracts.Message
+import dev.inmo.tgbotapi.types.update.MessageUpdate
+import dev.inmo.tgbotapi.types.update.abstracts.Update
+import ksm.StateController
+import ksm.context.StateContext
+import ksm.ktgbotapi.plugin.TelegramBotApiPlugin
+import ksm.plugin.plugin
+import ksm.state.StateScope
+
+public val StateContext.update: Update get() {
+ return plugin(TelegramBotApiPlugin).update
+}
+
+public val StateContext.messageUpdate: MessageUpdate get() {
+ return update as? MessageUpdate ?: error("Received update is not MessageUpdate")
+}
+
+public val StateContext.message: Message get() {
+ return messageUpdate.data
+}
+
+public val StateContext.telegramBot: TelegramBot get() {
+ return plugin(TelegramBotApiPlugin).telegramBot
+}
+
+internal val StateContext.executeBlock: suspend StateScope.() -> Unit get() {
+ return plugin(TelegramBotApiPlugin).executeBlock(context = this)
+}
+
+public fun StateContext.setExecuteBlock(block: suspend StateScope.() -> Unit) {
+ plugin(TelegramBotApiPlugin).setExecuteBlock(
+ context = this,
+ block = block
+ )
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateScope.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateScope.kt
new file mode 100644
index 0000000..0822519
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/StateScope.kt
@@ -0,0 +1,42 @@
+package ksm.ktgbotapi
+
+import dev.inmo.tgbotapi.bot.TelegramBot
+import dev.inmo.tgbotapi.extensions.api.send.sendMessage
+import dev.inmo.tgbotapi.extensions.utils.extensions.sourceChat
+import dev.inmo.tgbotapi.extensions.utils.extensions.sourceUser
+import dev.inmo.tgbotapi.types.IdChatIdentifier
+import dev.inmo.tgbotapi.types.buttons.KeyboardMarkup
+import dev.inmo.tgbotapi.types.chat.User
+import dev.inmo.tgbotapi.types.update.abstracts.Update
+import dev.inmo.tgbotapi.utils.PreviewFeature
+import ksm.ktgbotapi.match.MatchScope
+import ksm.state.StateScope
+
+public val StateScope.update: Update get() {
+ return controller.context.update
+}
+
+@OptIn(PreviewFeature::class)
+public val StateScope.user: User get() {
+ return update.sourceUser() ?: error("Cannot get user from $update")
+}
+
+@OptIn(PreviewFeature::class)
+public val StateScope.chatId: IdChatIdentifier get() {
+ return update.sourceChat()?.id ?: error("Cannot get chatId from $update")
+}
+
+public val StateScope.telegramBot: TelegramBot get() {
+ return controller.context.telegramBot
+}
+
+public suspend fun StateScope.sendMessage(
+ text: String,
+ replyMarkup: KeyboardMarkup? = null
+) {
+ telegramBot.sendMessage(chatId, text, replyMarkup = replyMarkup)
+}
+
+public inline fun StateScope.matchMessage(block: MatchScope.() -> Unit) {
+ MatchScope(controller.context).apply(block)
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/TelegramBotKSM.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/TelegramBotKSM.kt
new file mode 100644
index 0000000..cc56382
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/TelegramBotKSM.kt
@@ -0,0 +1,56 @@
+package ksm.ktgbotapi
+
+import dev.inmo.tgbotapi.bot.TelegramBot
+import dev.inmo.tgbotapi.types.update.abstracts.Update
+import kotlinx.coroutines.flow.Flow
+import ksm.StateController
+import ksm.builder.StateControllerBuilder
+import ksm.createRawStateController
+import ksm.ktgbotapi.plugin.TelegramBotApiPlugin
+import ksm.serialization.restore
+import ksm.stack.lastContext
+import ksm.stack.lastContextOrNull
+import ksm.stack.nextContext
+import ksm.stack.nextContextOrNull
+import ksm.state.StateScope
+import ksm.state.launch
+
+public class TelegramBotStateMachine(
+ private val builder: StateControllerBuilder.() -> Unit
+) {
+ public suspend fun start(
+ startStateName: String,
+ telegramBot: TelegramBot,
+ updates: Flow,
+ key: (T) -> TelegramPeerKey
+ ) {
+ updates.collect { update ->
+ val controller = createStateController(key(update), update, telegramBot)
+ if (controller.context.lastContextOrNull == null) {
+ controller.launch(startStateName)
+ }
+ // todo: это дефолтный интерцептор, надо дать возможность
+ // добавлять свои интерцепторы на апдейты после восстановления состояния
+ // -
+ // Новых абстракций можно не придумывать, а в случае, когда необходимо блокировать
+ // выполнение цепочки, можно сделать декоратор
+ val lastContext = controller.context.lastContext
+ val lastController = StateController(lastContext)
+ val stateScope = StateScope(lastController)
+ lastContext.executeBlock.invoke(stateScope)
+ }
+ }
+
+ private fun createStateController(
+ key: TelegramPeerKey,
+ update: Update,
+ telegramBot: TelegramBot
+ ): StateController {
+ return createRawStateController {
+ install(TelegramBotApiPlugin(key, update, telegramBot))
+ builder()
+ }.apply {
+ context.restore()
+ }
+ }
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/TelegramPeerKey.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/TelegramPeerKey.kt
new file mode 100644
index 0000000..21096db
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/TelegramPeerKey.kt
@@ -0,0 +1,6 @@
+package ksm.ktgbotapi
+
+import kotlin.jvm.JvmInline
+
+@JvmInline
+public value class TelegramPeerKey(public val string: String)
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/Command.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/Command.kt
new file mode 100644
index 0000000..58baa07
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/Command.kt
@@ -0,0 +1,12 @@
+package ksm.ktgbotapi.match
+
+public fun MatchScope.command(
+ text: String,
+ args: Int? = null,
+ prefix: String = "/",
+ block: (List) -> Unit
+) {
+ // todo: сделать реализацию, которая будет учитывать количество аргументов и т.п.
+ // возможно стоит тут задействовать интерцепторы и через них прокидывать дефолтное сообщение,
+ // когда команда неизвестна
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/Exact.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/Exact.kt
new file mode 100644
index 0000000..9c81eb5
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/Exact.kt
@@ -0,0 +1,13 @@
+package ksm.ktgbotapi.match
+
+import dev.inmo.tgbotapi.extensions.utils.extensions.raw.text
+import dev.inmo.tgbotapi.utils.RiskFeature
+import ksm.ktgbotapi.message
+
+@OptIn(RiskFeature::class)
+public inline fun MatchScope.exact(
+ text: String,
+ block: () -> Unit
+) {
+ if (context.message.text == text) block()
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/MatchScope.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/MatchScope.kt
new file mode 100644
index 0000000..b5fa8fa
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/match/MatchScope.kt
@@ -0,0 +1,24 @@
+package ksm.ktgbotapi.match
+
+import ksm.context.StateContext
+
+public class MatchScope(
+ public val context: StateContext
+) {
+ private var default: () -> Unit = { }
+
+ public fun default(block: () -> Unit) {
+ default = block
+ }
+
+ private var intercepted: Boolean = false
+
+ public fun intercept() {
+ require(!intercepted)
+ intercepted = true
+ }
+
+ public fun runDefault() {
+ if (!intercepted) default()
+ }
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/plugin/TelegramBotApiPlugin.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/plugin/TelegramBotApiPlugin.kt
new file mode 100644
index 0000000..cef2631
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/plugin/TelegramBotApiPlugin.kt
@@ -0,0 +1,46 @@
+package ksm.ktgbotapi.plugin
+
+import dev.inmo.tgbotapi.bot.TelegramBot
+import dev.inmo.tgbotapi.types.update.abstracts.Update
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.ktgbotapi.TelegramPeerKey
+import ksm.plugin.Plugin
+import ksm.state.StateScope
+
+public class TelegramBotApiPlugin(
+ public val peerKey: TelegramPeerKey,
+ public val update: Update,
+ public val telegramBot: TelegramBot
+) : Plugin {
+ override val key: Companion = TelegramBotApiPlugin
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration)
+ return context
+ }
+
+ private object Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ return context + TelegramBotEntry()
+ }
+ }
+
+ public fun setExecuteBlock(
+ context: StateContext,
+ block: suspend StateScope.() -> Unit
+ ) {
+ context.require(TelegramBotEntry).execute = block
+ }
+
+ public fun executeBlock(context: StateContext): suspend StateScope.() -> Unit {
+ return context.require(TelegramBotEntry).execute
+ ?: error("Please invoke `execute` in StateBuilder")
+ }
+
+ public companion object : StateContext.Key
+}
diff --git a/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/plugin/TelegramBotEntry.kt b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/plugin/TelegramBotEntry.kt
new file mode 100644
index 0000000..97032a7
--- /dev/null
+++ b/ktgbotapi/src/commonMain/kotlin/ksm/ktgbotapi/plugin/TelegramBotEntry.kt
@@ -0,0 +1,12 @@
+package ksm.ktgbotapi.plugin
+
+import ksm.context.StateContext
+import ksm.state.StateScope
+
+internal class TelegramBotEntry : StateContext.Element {
+ override val key = TelegramBotEntry
+
+ var execute: (suspend StateScope.() -> Unit)? = null
+
+ companion object : StateContext.Key
+}
diff --git a/mdi/build.gradle.kts b/mdi/build.gradle.kts
new file mode 100644
index 0000000..a712582
--- /dev/null
+++ b/mdi/build.gradle.kts
@@ -0,0 +1,10 @@
+plugins {
+ id("kmp-library-convention")
+}
+
+version = libs.versions.ksm.get()
+
+dependencies {
+ commonMainApi(projects.core)
+ commonMainImplementation(libs.mdi)
+}
diff --git a/mdi/src/commonMain/kotlin/ksm/mdi/StateBuilderScope.kt b/mdi/src/commonMain/kotlin/ksm/mdi/StateBuilderScope.kt
new file mode 100644
index 0000000..f4ae7f0
--- /dev/null
+++ b/mdi/src/commonMain/kotlin/ksm/mdi/StateBuilderScope.kt
@@ -0,0 +1,8 @@
+package ksm.mdi
+
+import app.meetacy.di.DI
+import ksm.state.builder.StateBuilderScope
+
+public var StateBuilderScope.di: DI
+ get() = context.di
+ set(value) { context.di = value }
diff --git a/mdi/src/commonMain/kotlin/ksm/mdi/StateContext.kt b/mdi/src/commonMain/kotlin/ksm/mdi/StateContext.kt
new file mode 100644
index 0000000..85f90d3
--- /dev/null
+++ b/mdi/src/commonMain/kotlin/ksm/mdi/StateContext.kt
@@ -0,0 +1,10 @@
+package ksm.mdi
+
+import app.meetacy.di.DI
+import ksm.mdi.plugin.DIPlugin
+import ksm.context.StateContext
+import ksm.plugin.plugin
+
+public var StateContext.di: DI
+ get() = plugin(DIPlugin).di(context = this)
+ set(value) = plugin(DIPlugin).setDI(context = this, di = value)
diff --git a/mdi/src/commonMain/kotlin/ksm/mdi/StateController.kt b/mdi/src/commonMain/kotlin/ksm/mdi/StateController.kt
new file mode 100644
index 0000000..ee15871
--- /dev/null
+++ b/mdi/src/commonMain/kotlin/ksm/mdi/StateController.kt
@@ -0,0 +1,8 @@
+package ksm.mdi
+
+import app.meetacy.di.DI
+import ksm.StateController
+
+public val StateController.di: DI get() {
+ return context.di
+}
diff --git a/mdi/src/commonMain/kotlin/ksm/mdi/plugin/DIEntry.kt b/mdi/src/commonMain/kotlin/ksm/mdi/plugin/DIEntry.kt
new file mode 100644
index 0000000..2d5484a
--- /dev/null
+++ b/mdi/src/commonMain/kotlin/ksm/mdi/plugin/DIEntry.kt
@@ -0,0 +1,22 @@
+package ksm.mdi.plugin
+
+import app.meetacy.di.DI
+import ksm.context.StateContext
+
+internal class DIEntry(private val root: DI) : StateContext.Element {
+ override val key = DIEntry
+
+ private var di: DI? = root
+
+ fun setDI(di: DI?) {
+ if (di == null) {
+ this.di = null
+ return
+ }
+ this.di = root + di
+ }
+
+ fun getDI() = di
+
+ companion object : StateContext.Key
+}
diff --git a/mdi/src/commonMain/kotlin/ksm/mdi/plugin/DIPlugin.kt b/mdi/src/commonMain/kotlin/ksm/mdi/plugin/DIPlugin.kt
new file mode 100644
index 0000000..4f3b4ff
--- /dev/null
+++ b/mdi/src/commonMain/kotlin/ksm/mdi/plugin/DIPlugin.kt
@@ -0,0 +1,50 @@
+package ksm.mdi.plugin
+
+import app.meetacy.di.DI
+import app.meetacy.di.builder.di
+import ksm.StateController
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.plugin.Plugin
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.lifecycle.addLifecycleInterceptor
+
+public class DIPlugin(private val root: DI) : Plugin {
+ override val key: Companion = DIPlugin
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration())
+ return context + DIEntry(root)
+ }
+
+ private inner class Configuration : ConfigurationInterceptor {
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ val entry = DIEntry(root)
+ context.addLifecycleInterceptor(Lifecycle(entry))
+ return context + entry
+ }
+ }
+
+ private inner class Lifecycle(val entry: DIEntry) : LifecycleInterceptor {
+ override fun onFinish(context: StateContext) {
+ entry.setDI(null)
+ }
+ }
+
+ public fun di(context: StateContext): DI {
+ val base = context.require(DIEntry).getDI() ?: error("DI is not initialized")
+ return base + di {
+ val stateController by constant(StateController(context))
+ }
+ }
+
+ public fun setDI(context: StateContext, di: DI) {
+ context.require(DIEntry).setDI(di)
+ }
+
+ public companion object : StateContext.Key
+}
diff --git a/navigation/compose-navigation/build.gradle.kts b/navigation/compose-navigation/build.gradle.kts
new file mode 100644
index 0000000..a9fffbd
--- /dev/null
+++ b/navigation/compose-navigation/build.gradle.kts
@@ -0,0 +1,26 @@
+plugins {
+ id("android-library-convention")
+}
+
+version = libs.versions.ksm.get()
+
+android {
+ namespace = "app.meetacy.ksm.androidx"
+
+ buildFeatures {
+ compose = true
+ }
+ composeOptions {
+ kotlinCompilerExtensionVersion = libs.versions.compose.compiler.get()
+ }
+}
+
+dependencies {
+ api(projects.mdi)
+ api(projects.kotlinxSerializationJson) // todo: remove; needed for example only
+ implementation(libs.kotlinx.serialization)
+ implementation(libs.mdi)
+ implementation(libs.compose.runtime)
+ implementation(libs.compose.foundation)
+ implementation(libs.compose.runtime.saveable)
+}
diff --git a/navigation/compose-navigation/src/main/kotlin/ksm/compose/RememberStateController.kt b/navigation/compose-navigation/src/main/kotlin/ksm/compose/RememberStateController.kt
new file mode 100644
index 0000000..c37f146
--- /dev/null
+++ b/navigation/compose-navigation/src/main/kotlin/ksm/compose/RememberStateController.kt
@@ -0,0 +1,32 @@
+package ksm.compose
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import app.meetacy.di.DI
+import app.meetacy.di.builder.di
+import ksm.StateController
+import ksm.builder.StateControllerBuilder
+import ksm.compose.plugin.ComposePlugin
+import ksm.compose.plugin.ComposeSerializationStore
+import ksm.createRawStateController
+import ksm.serialization.restore
+
+@Composable
+public fun rememberStateController(
+ builder: StateControllerBuilder.() -> Unit
+): StateController {
+ val store = rememberSaveable(
+ builder,
+ saver = ComposeSerializationStore.Saver,
+ init = { ComposeSerializationStore() }
+ )
+ return remember(store) {
+ createRawStateController {
+ install(ComposePlugin(store))
+ builder()
+ }.apply {
+ context.restore()
+ }
+ }
+}
diff --git a/navigation/compose-navigation/src/main/kotlin/ksm/compose/StateBuilderScope.kt b/navigation/compose-navigation/src/main/kotlin/ksm/compose/StateBuilderScope.kt
new file mode 100644
index 0000000..e16ce0a
--- /dev/null
+++ b/navigation/compose-navigation/src/main/kotlin/ksm/compose/StateBuilderScope.kt
@@ -0,0 +1,13 @@
+package ksm.compose
+
+import androidx.compose.runtime.Composable
+import ksm.compose.plugin.ComposePlugin
+import ksm.plugin.plugin
+import ksm.state.StateScope
+import ksm.state.builder.StateBuilderScope
+
+public fun StateBuilderScope.Content(
+ block: @Composable StateScope.() -> Unit
+) {
+ context.plugin(ComposePlugin).setContent(context, block)
+}
diff --git a/navigation/compose-navigation/src/main/kotlin/ksm/compose/host/StateHost.kt b/navigation/compose-navigation/src/main/kotlin/ksm/compose/host/StateHost.kt
new file mode 100644
index 0000000..1c40b5b
--- /dev/null
+++ b/navigation/compose-navigation/src/main/kotlin/ksm/compose/host/StateHost.kt
@@ -0,0 +1,32 @@
+package ksm.compose.host
+
+import androidx.compose.foundation.layout.Box
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.key
+import androidx.compose.runtime.remember
+import ksm.StateController
+import ksm.compose.plugin.ComposePlugin
+import ksm.plugin.plugin
+import ksm.state.StateScope
+import ksm.state.launch
+
+@Composable
+public fun StateHost(
+ controller: StateController,
+ startStateName: String
+) {
+ Box {
+ val currentContext = remember(controller) {
+ controller.launch(startStateName)
+ controller.context.plugin(ComposePlugin).currentContext
+ }.value
+
+ key(currentContext) {
+ currentContext ?: return
+ val childController = StateController(currentContext)
+ val contentScope = StateScope(childController)
+ val content = currentContext.plugin(ComposePlugin).content(currentContext)
+ content?.invoke(contentScope)
+ }
+ }
+}
diff --git a/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposeEntry.kt b/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposeEntry.kt
new file mode 100644
index 0000000..f897d45
--- /dev/null
+++ b/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposeEntry.kt
@@ -0,0 +1,13 @@
+package ksm.compose.plugin
+
+import androidx.compose.runtime.Composable
+import ksm.context.StateContext
+import ksm.state.StateScope
+
+internal class ComposeEntry : StateContext.Element {
+ override val key = ComposeEntry
+
+ var content: (@Composable StateScope.() -> Unit)? = null
+
+ companion object : StateContext.Key
+}
diff --git a/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposePlugin.kt b/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposePlugin.kt
new file mode 100644
index 0000000..44b281e
--- /dev/null
+++ b/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposePlugin.kt
@@ -0,0 +1,55 @@
+package ksm.compose.plugin
+
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.mutableStateOf
+import ksm.annotation.MutateContext
+import ksm.context.StateContext
+import ksm.context.configuration.interceptor.ConfigurationInterceptor
+import ksm.context.configuration.interceptor.addConfigurationInterceptor
+import ksm.lifecycle.LifecycleInterceptor
+import ksm.lifecycle.addLifecycleInterceptor
+import ksm.plugin.Plugin
+import ksm.serialization.BaseSerializationStore
+import ksm.state.StateScope
+
+public class ComposePlugin(
+ private val store: BaseSerializationStore.String
+) : Plugin {
+ override val key: Companion = ComposePlugin
+
+ internal val currentContext = mutableStateOf(value = null)
+
+ @MutateContext
+ override fun install(context: StateContext): StateContext {
+ context.addConfigurationInterceptor(Configuration())
+ return context + store
+ }
+
+ private inner class Configuration : ConfigurationInterceptor {
+
+ @MutateContext
+ override fun onConfigure(context: StateContext): StateContext {
+ context.addLifecycleInterceptor(Lifecycle())
+ return context + ComposeEntry()
+ }
+ }
+
+ private inner class Lifecycle : LifecycleInterceptor {
+ override fun onResume(context: StateContext) {
+ currentContext.value = context
+ }
+ }
+
+ internal fun setContent(
+ context: StateContext,
+ content: @Composable StateScope.() -> Unit
+ ) {
+ context.require(ComposeEntry).content = content
+ }
+
+ internal fun content(context: StateContext): (@Composable StateScope.() -> Unit)? {
+ return context.require(ComposeEntry).content
+ }
+
+ public companion object : StateContext.Key
+}
diff --git a/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposeSerializationStore.kt b/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposeSerializationStore.kt
new file mode 100644
index 0000000..bd58129
--- /dev/null
+++ b/navigation/compose-navigation/src/main/kotlin/ksm/compose/plugin/ComposeSerializationStore.kt
@@ -0,0 +1,25 @@
+package ksm.compose.plugin
+
+import androidx.compose.runtime.saveable.SaverScope
+import ksm.serialization.BaseSerializationStore
+import androidx.compose.runtime.saveable.Saver as ComposeSaver
+
+public class ComposeSerializationStore(
+ internal var string: String? = null
+) : BaseSerializationStore.String {
+
+ override fun apply(string: String) {
+ this.string = string
+ }
+
+ override fun get(): String? = string
+
+ public object Saver : ComposeSaver {
+ override fun restore(value: String): ComposeSerializationStore {
+ return ComposeSerializationStore(value)
+ }
+ override fun SaverScope.save(value: ComposeSerializationStore): String? {
+ return value.string
+ }
+ }
+}
diff --git a/settings.gradle.kts b/settings.gradle.kts
new file mode 100644
index 0000000..a2c5ece
--- /dev/null
+++ b/settings.gradle.kts
@@ -0,0 +1,36 @@
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
+
+rootProject.name = "ksm"
+
+pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ mavenCentral()
+ google()
+ }
+}
+
+dependencyResolutionManagement {
+ repositories {
+ mavenCentral()
+ google()
+ maven {
+ url = uri("https://maven.pkg.github.com/meetacy/di")
+ credentials {
+ username = System.getenv("GITHUB_USERNAME")
+ password = System.getenv("GITHUB_TOKEN")
+ }
+ }
+ }
+}
+
+includeBuild("build-logic")
+
+include(
+ "core",
+ "kotlinx-coroutines",
+ "kotlinx-serialization-json",
+ "navigation:compose-navigation",
+ "ktgbotapi",
+ "mdi"
+)