├── .editorconfig ├── .github └── workflows │ └── build.yml ├── .gitignore ├── .idea ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── copyright │ ├── open_jumpco.xml │ └── profiles_settings.xml ├── kotlinc.xml ├── misc.xml └── vcs.xml ├── LICENSE ├── README.adoc ├── build-linux.sh ├── build.cmd ├── build.gradle ├── generated ├── packet-reader-detail-simple.plantuml ├── packet-reader-detail.adoc ├── packet-reader-detail.plantuml ├── packet-reader.plantuml ├── paying-turnstile-detail-simple.plantuml ├── paying-turnstile-detail.adoc ├── paying-turnstile-detail.plantuml ├── paying-turnstile.plantuml ├── secure-turnstile-detail-simple.plantuml ├── secure-turnstile-detail.adoc ├── secure-turnstile-detail.plantuml ├── secure-turnstile.plantuml ├── timeout-turnstile-detail-simple.plantuml ├── timeout-turnstile-detail.adoc ├── timeout-turnstile-detail.plantuml ├── turnstile-detail-simple.plantuml ├── turnstile-detail.adoc ├── turnstile-detail.plantuml └── turnstile.plantuml ├── gradle.properties ├── gradle ├── docs.gradle ├── platform.gradle ├── publish.gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── kfsm-fsm.png ├── packet_reader.png ├── paying_turnstile.png ├── publish-linux.sh ├── publish.cmd ├── publish.sh ├── qodana.cmd ├── secure_turnstile.png ├── settings.gradle ├── src ├── commonMain │ └── kotlin │ │ └── io │ │ └── jumpco │ │ └── open │ │ └── kfsm │ │ ├── CommonTypes.kt │ │ ├── DefaultSyncTransition.kt │ │ ├── DslStateMachineHandler.kt │ │ ├── DslStateMapDefaultEventHandler.kt │ │ ├── DslStateMapEventHandler.kt │ │ ├── DslStateMapHandler.kt │ │ ├── SimpleSyncTransition.kt │ │ ├── StateMachineBuilder.kt │ │ ├── StateMachineDefinition.kt │ │ ├── StateMachineInstance.kt │ │ ├── StateMapBuilder.kt │ │ ├── StateMapDefinition.kt │ │ ├── StateMapInstance.kt │ │ ├── SyncGuardedTransition.kt │ │ ├── SyncTransition.kt │ │ ├── SyncTransitionRules.kt │ │ ├── async │ │ ├── AsyncDslStateMachineHandler.kt │ │ ├── AsyncDslStateMapDefaultEventHandler.kt │ │ ├── AsyncDslStateMapEventHandler.kt │ │ ├── AsyncDslStateMapHandler.kt │ │ ├── AsyncGuardedTransition.kt │ │ ├── AsyncStateMachineBuilder.kt │ │ ├── AsyncStateMachineDefinition.kt │ │ ├── AsyncStateMachineInstance.kt │ │ ├── AsyncStateMapBuilder.kt │ │ ├── AsyncStateMapDefinition.kt │ │ ├── AsyncStateMapInstance.kt │ │ ├── AsyncTimer.kt │ │ ├── AsyncTimerDefinition.kt │ │ ├── AsyncTransition.kt │ │ ├── AsyncTransitionRules.kt │ │ ├── DefaultAsyncTransition.kt │ │ ├── SimpleAsyncTransition.kt │ │ └── asyncStateMachines.kt │ │ └── stateMachine.kt ├── commonTest │ └── kotlin │ │ └── io │ │ └── jumpco │ │ └── open │ │ └── kfsm │ │ └── example │ │ ├── CarFSMTest.kt │ │ ├── DetailMockedTests.kt │ │ ├── DetailTests.kt │ │ ├── ImmutableLockFSM.kt │ │ ├── KeyboardBuffer.kt │ │ ├── LockFsmTests.kt │ │ ├── LockTypes.kt │ │ ├── PacketReaderTests.kt │ │ ├── PayingTurnstileFsmTests.kt │ │ ├── PayingTurnstileTypes.kt │ │ ├── SecureTurnstile.kt │ │ ├── SecureTurnstileTests.kt │ │ ├── TimeoutSecureTurnstile.kt │ │ ├── TurnstileFsmTests.kt │ │ └── TurnstileTypes.kt ├── docs │ ├── asciidoc │ │ ├── docinfo-footer.html │ │ ├── docinfo.html │ │ ├── documentation.adoc │ │ ├── index.adoc │ │ ├── kfsm.adoc │ │ ├── lock-fsm.png │ │ ├── lock_fsm.png │ │ ├── packet-reader-fsm-guard.png │ │ ├── packet-reader-fsm.png │ │ ├── packet_reader_detail.png │ │ ├── paying-turnstile-fsm.png │ │ ├── paying_turnstile_detail.png │ │ ├── prism.css │ │ ├── prism.js │ │ ├── sample-fsm.png │ │ ├── secure-turnstile-fsm.png │ │ ├── secure_turnstile_detail.png │ │ ├── statemachine-classes.png │ │ ├── statemachine-model.png │ │ ├── statemachine-packaged.png │ │ ├── statemachine-sequence.png │ │ ├── timeout_turnstile_detail.png │ │ ├── turnstile-fsm.png │ │ ├── turnstile-scxml.xml │ │ ├── turnstile-sequence.png │ │ ├── turnstile_detail.png │ │ ├── turnstile_fsm.png │ │ ├── turnstile_scxml.png │ │ └── useless-fsm.png │ └── plantuml │ │ ├── kfsm.plantuml │ │ ├── lock-fsm.plantuml │ │ ├── packet-reader-fsm-guard.plantuml │ │ ├── packet-reader-fsm.plantuml │ │ ├── paying-turnstile-fsm.plantuml │ │ ├── sample-fsm.plantuml │ │ ├── secure-turnstile-fsm.plantuml │ │ ├── statemachine-classes.plantuml │ │ ├── statemachine-model.plantuml │ │ ├── statemachine-packaged.plantuml │ │ ├── statemachine-sequence.plantuml │ │ ├── turnstile-fsm.plantuml │ │ ├── turnstile-sequence.plantuml │ │ └── useless-fsm.plantuml ├── jvmTest │ └── kotlin │ │ └── io │ │ └── jumpco │ │ └── open │ │ └── kfsm │ │ └── example │ │ ├── TimeoutSecureTurnstileTests.kt │ │ └── VisualizeTurnstileTest.kt └── nativeTest │ └── kotlin │ └── io │ └── jumpco │ └── open │ └── kfsm │ └── example │ └── TimeoutSecureTurnstileTests.kt └── turnstile.png /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | 5 | # We recommend you to keep these unchanged 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | # Change these settings to your own preference 12 | indent_style = space 13 | indent_size = 4 14 | 15 | [*.{kt,java,kts}] 16 | indent_style = space 17 | indent_size = 4 18 | 19 | [*.{ts,tsx,js,jsx,json,css,scss,yml}] 20 | indent_size = 2 21 | 22 | [*.md] 23 | trim_trailing_whitespace = false 24 | 25 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: 'Build' 2 | 3 | on: 4 | push: 5 | workflow_dispatch: 6 | 7 | jobs: 8 | build: 9 | strategy: 10 | matrix: 11 | # os: [ 'ubuntu', 'macos', 'windows' ] 12 | os: [ 'ubuntu', 'windows', 'macos' ] 13 | runs-on: '${{ matrix.os }}-latest' 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: browser-actions/setup-chrome@v1 17 | - id: parameters 18 | shell: bash 19 | run: | 20 | OS="${{matrix.os}}" 21 | case $OS in 22 | "ubuntu") 23 | echo "arch=x86" >> $GITHUB_OUTPUT 24 | echo "profile=jvm,js,linux,wasm-js" >> $GITHUB_OUTPUT 25 | echo "dokka=true" >> $GITHUB_OUTPUT 26 | ;; 27 | "windows") 28 | echo "arch=x86" >> $GITHUB_OUTPUT 29 | echo "profile=mingw" >> $GITHUB_OUTPUT 30 | echo "dokka=false" >> $GITHUB_OUTPUT 31 | ;; 32 | "macos") 33 | echo "arch=arm64" >> $GITHUB_OUTPUT 34 | echo "profile=macos" >> $GITHUB_OUTPUT 35 | echo "dokka=false" >> $GITHUB_OUTPUT 36 | ;; 37 | *) 38 | echo "Unable to determine arch from $OS" 39 | exit 1 40 | ;; 41 | esac 42 | - name: Set up JDK 11 for ${{ steps.parameters.outputs.arch }} 43 | uses: actions/setup-java@v3 44 | with: 45 | java-version: '17' 46 | distribution: 'zulu' 47 | cache: gradle 48 | - name: 'Build' 49 | shell: bash 50 | run: | 51 | chmod a+x ./gradlew 52 | echo "::info ::Build profile=${{ steps.parameters.outputs.profile }} on ${{ steps.parameters.outputs.arch }}" 53 | ./gradlew build -PbuildProfile=${{ steps.parameters.outputs.profile }} -Pdokka=${{ steps.parameters.outputs.dokka }} 54 | - name: 'Test Report - ${{ matrix.os }}' 55 | if: ${{ success() || failure() }} 56 | uses: dorny/test-reporter@v1 57 | with: 58 | name: 'Test Report - ${{ matrix.os }}' 59 | path: '**/test-results/**/*.xml' 60 | reporter: java-junit 61 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ### Gradle template 3 | .gradle 4 | build/ 5 | 6 | # Ignore Gradle GUI config 7 | gradle-app.setting 8 | 9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 10 | !gradle-wrapper.jar 11 | 12 | # Cache of project 13 | .gradletasknamecache 14 | 15 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 16 | # gradle/wrapper/gradle-wrapper.properties 17 | 18 | ### JetBrains template 19 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 20 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 21 | 22 | # User-specific stuff 23 | .idea/**/workspace.xml 24 | .idea/**/tasks.xml 25 | .idea/**/usage.statistics.xml 26 | .idea/**/dictionaries 27 | .idea/**/shelf 28 | 29 | # Generated files 30 | .idea/**/contentModel.xml 31 | 32 | # Sensitive or high-churn files 33 | .idea/**/dataSources/ 34 | .idea/**/dataSources.ids 35 | .idea/**/dataSources.local.xml 36 | .idea/**/sqlDataSources.xml 37 | .idea/**/dynamic.xml 38 | .idea/**/uiDesigner.xml 39 | .idea/**/dbnavigator.xml 40 | 41 | # Gradle 42 | .idea/**/gradle.xml 43 | .idea/**/libraries 44 | 45 | # Gradle and Maven with auto-import 46 | # When using Gradle or Maven with auto-import, you should exclude module files, 47 | # since they will be recreated, and may cause churn. Uncomment if using 48 | # auto-import. 49 | # .idea/modules.xml 50 | # .idea/*.iml 51 | # .idea/modules 52 | # *.iml 53 | # *.ipr 54 | 55 | # CMake 56 | cmake-build-*/ 57 | 58 | # Mongo Explorer plugin 59 | .idea/**/mongoSettings.xml 60 | 61 | # File-based project format 62 | *.iws 63 | 64 | # IntelliJ 65 | out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Cursive Clojure plugin 74 | .idea/replstate.xml 75 | 76 | # Crashlytics plugin (for Android Studio and IntelliJ) 77 | com_crashlytics_export_strings.xml 78 | crashlytics.properties 79 | crashlytics-build.properties 80 | fabric.properties 81 | 82 | # Editor-based Rest Client 83 | .idea/httpRequests 84 | 85 | # Android studio 3.1+ serialized cache file 86 | .idea/caches/build_file_checksums.ser 87 | 88 | ### Kotlin template 89 | # Compiled class file 90 | *.class 91 | 92 | # Log file 93 | *.log 94 | 95 | # BlueJ files 96 | *.ctxt 97 | 98 | # Mobile Tools for Java (J2ME) 99 | .mtj.tmp/ 100 | 101 | # Package Files # 102 | *.jar 103 | *.war 104 | *.nar 105 | *.ear 106 | *.zip 107 | *.tar.gz 108 | *.rar 109 | 110 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 111 | hs_err_pid* 112 | 113 | .idea/ 114 | codemr/ 115 | kotlin-js-store/ 116 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 13 | 14 | 15 | 17 | 18 | 19 | 30 | 31 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/open_jumpco.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /.idea/copyright/profiles_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 19 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2021 Open JumpCO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ./gradlew -i --continue build publishToMavenLocal -x dokkaJavadoc -x dokkaGfm -x dokkaJekyll 3 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | gradlew -S -i clean build -x dokkaJavadoc -x dokkaGfm -x dokkaJekyll 3 | -------------------------------------------------------------------------------- /generated/packet-reader-detail-simple.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state PacketReaderFSM { 10 | [*] --> RCVPCKT : CTRL 11 | RCVPCKT --> RCVDATA : CTRL 12 | RCVPCKT --> RCVCHK : BYTE 13 | RCVDATA --> RCVDATA : BYTE 14 | RCVDATA --> RCVPCKT : CTRL 15 | RCVDATA --> RCVESC : ESC 16 | RCVESC --> RCVDATA : ESC 17 | RCVESC --> RCVDATA : CTRL 18 | RCVCHK --> RCVCHK : BYTE 19 | RCVCHK --> RCVCHKESC : ESC 20 | RCVCHK --> CHKSUM : CTRL 21 | CHKSUM --> [*] : <> 22 | CHKSUM --> [*] : <> 23 | RCVCHKESC --> RCVCHK : ESC 24 | RCVCHKESC --> RCVCHK : CTRL 25 | } 26 | @enduml 27 | -------------------------------------------------------------------------------- /generated/packet-reader-detail.adoc: -------------------------------------------------------------------------------- 1 | == PacketReaderFSM State Chart 2 | 3 | === PacketReaderFSM State Map 4 | 5 | |=== 6 | | Start | Event[Guard] | Target | Action 7 | 8 | | START 9 | | CTRL `[{byte->byte == CharacterConstants.SOH}]` 10 | | RCVPCKT 11 | a| 12 | 13 | | RCVPCKT 14 | | CTRL `[{byte->byte == CharacterConstants.STX}]` 15 | | RCVDATA 16 | a| [source,kotlin] 17 | ---- 18 | { 19 | addField() 20 | } 21 | ---- 22 | 23 | | RCVPCKT 24 | | BYTE 25 | | RCVCHK 26 | a| [source,kotlin] 27 | ---- 28 | {byte-> 29 | requireNotNull(byte) 30 | addChecksum(byte) 31 | } 32 | ---- 33 | 34 | | RCVDATA 35 | | BYTE 36 | | RCVDATA 37 | a| [source,kotlin] 38 | ---- 39 | {byte-> 40 | requireNotNull(byte) 41 | addByte(byte) 42 | } 43 | ---- 44 | 45 | | RCVDATA 46 | | CTRL `[{byte->byte == CharacterConstants.ETX}]` 47 | | RCVPCKT 48 | a| [source,kotlin] 49 | ---- 50 | { 51 | endField() 52 | } 53 | ---- 54 | 55 | | RCVDATA 56 | | ESC 57 | | RCVESC 58 | a| 59 | 60 | | RCVESC 61 | | ESC 62 | | RCVDATA 63 | a| [source,kotlin] 64 | ---- 65 | { 66 | addByte(CharacterConstants.ESC) 67 | } 68 | ---- 69 | 70 | | RCVESC 71 | | CTRL 72 | | RCVDATA 73 | a| [source,kotlin] 74 | ---- 75 | {byte-> 76 | requireNotNull(byte) 77 | addByte(byte) 78 | } 79 | ---- 80 | 81 | | RCVCHK 82 | | BYTE 83 | | RCVCHK 84 | a| [source,kotlin] 85 | ---- 86 | {byte-> 87 | requireNotNull(byte) 88 | addChecksum(byte) 89 | } 90 | ---- 91 | 92 | | RCVCHK 93 | | ESC 94 | | RCVCHKESC 95 | a| 96 | 97 | | RCVCHK 98 | | CTRL `[{byte->byte == CharacterConstants.EOT}]` 99 | | CHKSUM 100 | a| [source,kotlin] 101 | ---- 102 | { 103 | checksum() 104 | } 105 | ---- 106 | 107 | | CHKSUM 108 | | \<> `[{!checksumValid}]` 109 | | [*] 110 | a| [source,kotlin] 111 | ---- 112 | { 113 | sendNACK() 114 | } 115 | ---- 116 | 117 | | CHKSUM 118 | | \<> `[{checksumValid}]` 119 | | [*] 120 | a| [source,kotlin] 121 | ---- 122 | { 123 | sendACK() 124 | } 125 | ---- 126 | 127 | | RCVCHKESC 128 | | ESC 129 | | RCVCHK 130 | a| [source,kotlin] 131 | ---- 132 | { 133 | addChecksum(CharacterConstants.ESC) 134 | } 135 | ---- 136 | 137 | | RCVCHKESC 138 | | CTRL 139 | | RCVCHK 140 | a| [source,kotlin] 141 | ---- 142 | {byte-> 143 | requireNotNull(byte) 144 | addChecksum(byte) 145 | } 146 | ---- 147 | |=== 148 | 149 | -------------------------------------------------------------------------------- /generated/packet-reader-detail.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state PacketReaderFSM { 10 | [*] --> RCVPCKT : CTRL [byte->byte == CharacterConstants.SOH] 11 | RCVPCKT --> RCVDATA : CTRL [byte->byte == CharacterConstants.STX] -> {\l addField()\l} 12 | RCVPCKT --> RCVCHK : BYTE -> {byte->\l requireNotNull(byte)\l addChecksum(byte)\l} 13 | RCVDATA --> RCVDATA : BYTE -> {byte->\l requireNotNull(byte)\l addByte(byte)\l} 14 | RCVDATA --> RCVPCKT : CTRL [byte->byte == CharacterConstants.ETX] -> {\l endField()\l} 15 | RCVDATA --> RCVESC : ESC 16 | RCVESC --> RCVDATA : ESC -> {\l addByte(CharacterConstants.ESC)\l} 17 | RCVESC --> RCVDATA : CTRL -> {byte->\l requireNotNull(byte)\l addByte(byte)\l} 18 | RCVCHK --> RCVCHK : BYTE -> {byte->\l requireNotNull(byte)\l addChecksum(byte)\l} 19 | RCVCHK --> RCVCHKESC : ESC 20 | RCVCHK --> CHKSUM : CTRL [byte->byte == CharacterConstants.EOT] -> {\l checksum()\l} 21 | CHKSUM --> [*] : <> [!checksumValid] -> {\l sendNACK()\l} 22 | CHKSUM --> [*] : <> [checksumValid] -> {\l sendACK()\l} 23 | RCVCHKESC --> RCVCHK : ESC -> {\l addChecksum(CharacterConstants.ESC)\l} 24 | RCVCHKESC --> RCVCHK : CTRL -> {byte->\l requireNotNull(byte)\l addChecksum(byte)\l} 25 | } 26 | @enduml 27 | -------------------------------------------------------------------------------- /generated/packet-reader.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state PacketReaderFSM { 10 | [*] --> RCVPCKT : CTRL [byte->byte == CharacterConstants.SOH] 11 | RCVPCKT --> RCVDATA : CTRL [byte->byte == CharacterConstants.STX] -> {\l addField()\l} 12 | RCVPCKT --> RCVCHK : BYTE -> {byte->\l requireNotNull(byte)\l addChecksum(byte)\l} 13 | RCVDATA --> RCVDATA : BYTE -> {byte->\l requireNotNull(byte)\l addByte(byte)\l} 14 | RCVDATA --> RCVPCKT : CTRL [byte->byte == CharacterConstants.ETX] -> {\l endField()\l} 15 | RCVDATA --> RCVESC : ESC 16 | RCVESC --> RCVDATA : ESC -> {\l addByte(CharacterConstants.ESC)\l} 17 | RCVESC --> RCVDATA : CTRL -> {byte->\l requireNotNull(byte)\l addByte(byte)\l} 18 | RCVCHK --> RCVCHK : BYTE -> {byte->\l requireNotNull(byte)\l addChecksum(byte)\l} 19 | RCVCHK --> RCVCHKESC : ESC 20 | RCVCHK --> CHKSUM : CTRL [byte->byte == CharacterConstants.EOT] -> {\l checksum()\l} 21 | CHKSUM --> [*] : <> [!checksumValid] -> {\l sendNACK()\l} 22 | CHKSUM --> [*] : <> [checksumValid] -> {\l sendACK()\l} 23 | RCVCHKESC --> RCVCHK : ESC -> {\l addChecksum(CharacterConstants.ESC)\l} 24 | RCVCHKESC --> RCVCHK : CTRL -> {byte->\l requireNotNull(byte)\l addChecksum(byte)\l} 25 | } 26 | @enduml 27 | -------------------------------------------------------------------------------- /generated/paying-turnstile-detail-simple.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state coins { 10 | COINS --> UNLOCKED : <> 11 | COINS --> UNLOCKED : <> 12 | COINS --> COINS : COIN 13 | } 14 | state PayingTurnstileFSM { 15 | [*] --> UNLOCKED 16 | LOCKED --> COINS : COIN 17 | LOCKED --> COINS : COIN 18 | UNLOCKED --> UNLOCKED : COIN 19 | UNLOCKED --> LOCKED : PASS 20 | } 21 | @enduml 22 | -------------------------------------------------------------------------------- /generated/paying-turnstile-detail.adoc: -------------------------------------------------------------------------------- 1 | == PayingTurnstileFSM State Chart 2 | 3 | === PayingTurnstileFSM State Map 4 | 5 | |=== 6 | | Start | Event[Guard] | Target | Action 7 | 8 | | <> 9 | | 10 | | UNLOCKED 11 | a| 12 | 13 | | LOCKED 14 | | COIN 15 | | COINS 16 | a| [source,kotlin] 17 | ---- 18 | {value-> 19 | requireNotNull(value){"argument required for COIN"} 20 | coin(value) 21 | unlock() 22 | reset() 23 | } 24 | ---- 25 | 26 | | LOCKED 27 | | COIN `[{value->requireNotNull(value){"argument required for COIN"};value+coins < requiredCoins;}]` 28 | | COINS 29 | a| [source,kotlin] 30 | ---- 31 | {value-> 32 | requireNotNull(value){"argument required for COIN"} 33 | println("PUSH TRANSITION") 34 | coin(value) 35 | println("Coins=$coins, Please add ${requiredCoins-coins}") 36 | } 37 | ---- 38 | 39 | | UNLOCKED 40 | | COIN 41 | | UNLOCKED 42 | a| [source,kotlin] 43 | ---- 44 | {value-> 45 | requireNotNull(value){"argument required for COIN"} 46 | returnCoin(coin(value)) 47 | } 48 | ---- 49 | 50 | | UNLOCKED 51 | | PASS 52 | | LOCKED 53 | a| [source,kotlin] 54 | ---- 55 | { 56 | lock() 57 | } 58 | ---- 59 | |=== 60 | 61 | === State Map coins 62 | 63 | |=== 64 | | Start | Event[Guard] | Target | Action 65 | 66 | | COINS 67 | | \<> `[{coins > requiredCoins}]` 68 | | UNLOCKED 69 | a| [source,kotlin] 70 | ---- 71 | { 72 | println("automaticPop:returnCoin") 73 | returnCoin(coins-requiredCoins) 74 | unlock() 75 | reset() 76 | } 77 | ---- 78 | 79 | | COINS 80 | | \<> `[{coins == requiredCoins}]` 81 | | UNLOCKED 82 | a| [source,kotlin] 83 | ---- 84 | { 85 | println("automaticPop") 86 | unlock() 87 | reset() 88 | } 89 | ---- 90 | 91 | | COINS 92 | | COIN 93 | | COINS 94 | a| [source,kotlin] 95 | ---- 96 | {value-> 97 | requireNotNull(value){"argument required for COIN"} 98 | coin(value) 99 | println("Coins=$coins") 100 | if(coins < requiredCoins){ 101 | println("Please add ${requiredCoins-coins}") 102 | } 103 | } 104 | ---- 105 | |=== 106 | 107 | -------------------------------------------------------------------------------- /generated/paying-turnstile-detail.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state coins { 10 | COINS --> UNLOCKED : <> [coins > requiredCoins] -> {\l println("automaticPop:returnCoin")\l returnCoin(coins-requiredCoins)\l unlock()\l reset()\l} 11 | COINS --> UNLOCKED : <> [coins == requiredCoins] -> {\l println("automaticPop")\l unlock()\l reset()\l} 12 | COINS --> COINS : COIN -> {value->\l requireNotNull(value){"argument required for COIN"}\l coin(value)\l println("Coins=$coins")\l if(coins < requiredCoins){\l println("Please add ${requiredCoins-coins}")\l }\l} 13 | } 14 | state PayingTurnstileFSM { 15 | [*] --> UNLOCKED 16 | LOCKED --> COINS : COIN -> {value->\l requireNotNull(value){"argument required for COIN"}\l coin(value)\l unlock()\l reset()\l} 17 | LOCKED --> COINS : COIN [value->\l requireNotNull(value){"argument required for COIN"};\l value+coins < requiredCoins;\l] -> {value->\l requireNotNull(value){"argument required for COIN"}\l println("PUSH TRANSITION")\l coin(value)\l println("Coins=$coins, Please add ${requiredCoins-coins}")\l} 18 | UNLOCKED --> UNLOCKED : COIN -> {value->\l requireNotNull(value){"argument required for COIN"}\l returnCoin(coin(value))\l} 19 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 20 | } 21 | note top of PayingTurnstileFSM 22 | <> {coins >= 0} 23 | end note 24 | @enduml 25 | -------------------------------------------------------------------------------- /generated/paying-turnstile.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state coins { 10 | COINS --> UNLOCKED : <> [coins > requiredCoins] -> {\l println("automaticPop:returnCoin")\l returnCoin(coins-requiredCoins)\l unlock()\l reset()\l} 11 | COINS --> UNLOCKED : <> [coins == requiredCoins] -> {\l println("automaticPop")\l unlock()\l reset()\l} 12 | COINS --> COINS : COIN -> {value->\l requireNotNull(value){"argument required for COIN"}\l coin(value)\l println("Coins=$coins")\l if(coins < requiredCoins){\l println("Please add ${requiredCoins-coins}")\l }\l} 13 | } 14 | state PayingTurnstileFSM { 15 | [*] --> UNLOCKED 16 | LOCKED --> COINS : COIN -> {value->\l requireNotNull(value){"argument required for COIN"}\l coin(value)\l unlock()\l reset()\l} 17 | LOCKED --> COINS : COIN [value->\l requireNotNull(value){"argument required for COIN"};\l value+coins < requiredCoins;\l] -> {value->\l requireNotNull(value){"argument required for COIN"}\l println("PUSH TRANSITION")\l coin(value)\l println("Coins=$coins, Please add ${requiredCoins-coins}")\l} 18 | UNLOCKED --> UNLOCKED : COIN -> {value->\l requireNotNull(value){"argument required for COIN"}\l returnCoin(coin(value))\l} 19 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 20 | } 21 | note top of PayingTurnstileFSM 22 | <> {coins >= 0} 23 | end note 24 | @enduml 25 | -------------------------------------------------------------------------------- /generated/secure-turnstile-detail-simple.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state SecureTurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> LOCKED : CARD 12 | LOCKED --> LOCKED : CARD 13 | LOCKED --> UNLOCKED : CARD 14 | LOCKED --> LOCKED : CARD 15 | UNLOCKED --> LOCKED : CARD 16 | UNLOCKED --> LOCKED : PASS 17 | } 18 | @enduml 19 | -------------------------------------------------------------------------------- /generated/secure-turnstile-detail.adoc: -------------------------------------------------------------------------------- 1 | == SecureTurnstileFSM State Chart 2 | 3 | === SecureTurnstileFSM State Map 4 | 5 | |=== 6 | | Start | Event[Guard] | Target | Action 7 | 8 | | <> 9 | | 10 | | LOCKED 11 | a| 12 | 13 | | LOCKED 14 | | CARD `[{cardId->requireNotNull(cardId);isOverrideCard(cardId)&&overrideActive;}]` 15 | | LOCKED 16 | a| [source,kotlin] 17 | ---- 18 | { 19 | cancelOverride() 20 | } 21 | ---- 22 | 23 | | LOCKED 24 | | CARD `[{cardId->requireNotNull(cardId);isOverrideCard(cardId);}]` 25 | | LOCKED 26 | a| [source,kotlin] 27 | ---- 28 | { 29 | activateOverride() 30 | } 31 | ---- 32 | 33 | | LOCKED 34 | | CARD `[{cardId->requireNotNull(cardId);overrideActive\|\|isValidCard(cardId);}]` 35 | | UNLOCKED 36 | a| [source,kotlin] 37 | ---- 38 | { 39 | unlock() 40 | } 41 | ---- 42 | 43 | | LOCKED 44 | | CARD `[{cardId->requireNotNull(cardId){"cardId is required"};!isValidCard(cardId);}]` 45 | | LOCKED 46 | a| [source,kotlin] 47 | ---- 48 | {cardId-> 49 | requireNotNull(cardId) 50 | invalidCard(cardId) 51 | } 52 | ---- 53 | 54 | | UNLOCKED 55 | | CARD `[{cardId->requireNotNull(cardId);isOverrideCard(cardId);}]` 56 | | LOCKED 57 | a| [source,kotlin] 58 | ---- 59 | { 60 | lock() 61 | } 62 | ---- 63 | 64 | | UNLOCKED 65 | | PASS 66 | | LOCKED 67 | a| [source,kotlin] 68 | ---- 69 | { 70 | lock() 71 | } 72 | ---- 73 | |=== 74 | 75 | -------------------------------------------------------------------------------- /generated/secure-turnstile-detail.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state SecureTurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId)&&overrideActive;\l] -> {\l cancelOverride()\l} 12 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId);\l] -> {\l activateOverride()\l} 13 | LOCKED --> UNLOCKED : CARD [cardId->\l requireNotNull(cardId);\l overrideActive||isValidCard(cardId);\l] -> {\l unlock()\l} 14 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId){"cardId is required"};\l !isValidCard(cardId);\l] -> {cardId->\l requireNotNull(cardId)\l invalidCard(cardId)\l} 15 | UNLOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId);\l] -> {\l lock()\l} 16 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 17 | } 18 | @enduml 19 | -------------------------------------------------------------------------------- /generated/secure-turnstile.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state SecureTurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId)&&overrideActive;\l] -> {\l cancelOverride()\l} 12 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId);\l] -> {\l activateOverride()\l} 13 | LOCKED --> UNLOCKED : CARD [cardId->\l requireNotNull(cardId);\l overrideActive||isValidCard(cardId);\l] -> {\l unlock()\l} 14 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId){"cardId is required"};\l !isValidCard(cardId);\l] -> {cardId->\l requireNotNull(cardId)\l invalidCard(cardId)\l} 15 | UNLOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId);\l] -> {\l lock()\l} 16 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 17 | } 18 | @enduml 19 | -------------------------------------------------------------------------------- /generated/timeout-turnstile-detail-simple.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state TimerSecureTurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> LOCKED : CARD 12 | LOCKED --> LOCKED : CARD 13 | LOCKED --> UNLOCKED : CARD 14 | LOCKED --> LOCKED : CARD 15 | UNLOCKED --> LOCKED : <> 16 | UNLOCKED --> LOCKED : CARD 17 | UNLOCKED --> LOCKED : PASS 18 | } 19 | @enduml 20 | -------------------------------------------------------------------------------- /generated/timeout-turnstile-detail.adoc: -------------------------------------------------------------------------------- 1 | == TimerSecureTurnstileFSM State Chart 2 | 3 | === TimerSecureTurnstileFSM State Map 4 | 5 | |=== 6 | | Start | Event[Guard] | Target | Action 7 | 8 | | <> 9 | | 10 | | LOCKED 11 | a| 12 | 13 | | LOCKED 14 | | CARD `[{cardId->requireNotNull(cardId);isOverrideCard(cardId)&&overrideActive;}]` 15 | | LOCKED 16 | a| [source,kotlin] 17 | ---- 18 | { 19 | cancelOverride() 20 | } 21 | ---- 22 | 23 | | LOCKED 24 | | CARD `[{cardId->requireNotNull(cardId);isOverrideCard(cardId);}]` 25 | | LOCKED 26 | a| [source,kotlin] 27 | ---- 28 | { 29 | activateOverride() 30 | } 31 | ---- 32 | 33 | | LOCKED 34 | | CARD `[{cardId->requireNotNull(cardId);overrideActive\|\|isValidCard(cardId);}]` 35 | | UNLOCKED 36 | a| [source,kotlin] 37 | ---- 38 | { 39 | unlock() 40 | } 41 | ---- 42 | 43 | | LOCKED 44 | | CARD `[{cardId->requireNotNull(cardId){"cardId is required"};!isValidCard(cardId);}]` 45 | | LOCKED 46 | a| [source,kotlin] 47 | ---- 48 | {cardId-> 49 | requireNotNull(cardId) 50 | invalidCard(cardId) 51 | } 52 | ---- 53 | 54 | | UNLOCKED 55 | | \<> 56 | | LOCKED 57 | a| [source,kotlin] 58 | ---- 59 | { 60 | println("Timeout. Locking") 61 | lock() 62 | } 63 | ---- 64 | 65 | | UNLOCKED 66 | | CARD `[{cardId->requireNotNull(cardId);isOverrideCard(cardId);}]` 67 | | LOCKED 68 | a| [source,kotlin] 69 | ---- 70 | { 71 | lock() 72 | } 73 | ---- 74 | 75 | | UNLOCKED 76 | | PASS 77 | | LOCKED 78 | a| [source,kotlin] 79 | ---- 80 | { 81 | lock() 82 | } 83 | ---- 84 | |=== 85 | 86 | -------------------------------------------------------------------------------- /generated/timeout-turnstile-detail.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state TimerSecureTurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId)&&overrideActive;\l] -> {\l cancelOverride()\l} 12 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId);\l] -> {\l activateOverride()\l} 13 | LOCKED --> UNLOCKED : CARD [cardId->\l requireNotNull(cardId);\l overrideActive||isValidCard(cardId);\l] -> {\l unlock()\l} 14 | LOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId){"cardId is required"};\l !isValidCard(cardId);\l] -> {cardId->\l requireNotNull(cardId)\l invalidCard(cardId)\l} 15 | UNLOCKED --> LOCKED : <> -> {\l println("Timeout. Locking")\l lock()\l} 16 | UNLOCKED --> LOCKED : CARD [cardId->\l requireNotNull(cardId);\l isOverrideCard(cardId);\l] -> {\l lock()\l} 17 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 18 | } 19 | @enduml 20 | -------------------------------------------------------------------------------- /generated/turnstile-detail-simple.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state TurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> UNLOCKED : COIN 12 | UNLOCKED --> UNLOCKED : COIN 13 | UNLOCKED --> LOCKED : PASS 14 | } 15 | @enduml 16 | -------------------------------------------------------------------------------- /generated/turnstile-detail.adoc: -------------------------------------------------------------------------------- 1 | == TurnstileFSM State Chart 2 | 3 | === TurnstileFSM State Map 4 | 5 | |=== 6 | | Start | Event[Guard] | Target | Action 7 | 8 | | <> 9 | | 10 | | LOCKED 11 | a| 12 | 13 | | LOCKED 14 | | COIN 15 | | UNLOCKED 16 | a| [source,kotlin] 17 | ---- 18 | { 19 | unlock() 20 | } 21 | ---- 22 | 23 | | UNLOCKED 24 | | COIN 25 | | UNLOCKED 26 | a| [source,kotlin] 27 | ---- 28 | { 29 | returnCoin() 30 | } 31 | ---- 32 | 33 | | UNLOCKED 34 | | PASS 35 | | LOCKED 36 | a| [source,kotlin] 37 | ---- 38 | { 39 | lock() 40 | } 41 | ---- 42 | |=== 43 | 44 | -------------------------------------------------------------------------------- /generated/turnstile-detail.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state TurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> UNLOCKED : COIN -> {\l unlock()\l} 12 | UNLOCKED --> UNLOCKED : COIN -> {\l returnCoin()\l} 13 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 14 | } 15 | @enduml 16 | -------------------------------------------------------------------------------- /generated/turnstile.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam StateFontName Helvetica 4 | skinparam defaultFontName Monospaced 5 | skinparam defaultFontStyle Bold 6 | skinparam state { 7 | FontStyle Bold 8 | } 9 | state TurnstileFSM { 10 | [*] --> LOCKED 11 | LOCKED --> UNLOCKED : COIN -> {\l unlock()\l} 12 | UNLOCKED --> UNLOCKED : COIN -> {\l returnCoin()\l} 13 | UNLOCKED --> LOCKED : PASS -> {\l lock()\l} 14 | } 15 | @enduml 16 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # org.gradle.caching=true 2 | org.gradle.jvmargs=-Xmx3g 3 | # kotlin.incremental.js=false 4 | kotlin.code.style=official 5 | kotlin.js.compiler=ir 6 | buildProfile=jvm,js,default,wasm-js 7 | # defaultProfile=jvm,default 8 | vcs=https://github.com/open-jumpco/kfsm.git 9 | 10 | signing.keyId=9B78DD13 11 | skipSign=true 12 | -------------------------------------------------------------------------------- /gradle/docs.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | apply plugin: 'org.jetbrains.dokka' 20 | apply plugin: 'org.asciidoctor.jvm.convert' 21 | 22 | 23 | asciidoctor { 24 | inputs.files(fileTree('src/commonTest/kotlin/*')) 25 | baseDirFollowsSourceDir() 26 | logDocuments = true 27 | asciidoctorj { 28 | fatalWarnings missingIncludes() 29 | } 30 | 31 | sources { 32 | include 'index.adoc' 33 | } 34 | 35 | resources { 36 | from(sourceDir) { 37 | include '**/*.png' 38 | include '**/*.xml' 39 | include '**/*.js' 40 | include '**/*.css' 41 | } 42 | } 43 | 44 | attributes toc: 'left', 45 | 'source-highlighter': 'prism', 46 | idprefix: '', 47 | idseparator: '-', 48 | docinfo1: '' 49 | 50 | requires ['asciidoctor-prism-extension'] 51 | 52 | } 53 | 54 | task docs(type: Zip, dependsOn: [asciidoctor, dokkaHtml]) { 55 | archiveBaseName = project.name 56 | archiveClassifier = 'doc' 57 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 58 | 59 | from(file('build/docs/asciidoc')) 60 | 61 | from(file('build/dokka/html')) { 62 | into('javadoc') 63 | } 64 | } 65 | 66 | publishing { 67 | publications { 68 | documentation(MavenPublication) { 69 | artifact docs 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /gradle/platform.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | import org.gradle.nativeplatform.platform.internal.DefaultNativePlatform 19 | 20 | apply plugin: 'org.jetbrains.kotlin.multiplatform' 21 | def targetList = ['mingw', 'linux', 'macos', 'js', 'jvm', 'wasm-js'] 22 | 23 | project.ext.useTarget = [:] 24 | def useTarget = project.ext.useTarget 25 | 26 | def arch = DefaultNativePlatform.currentArchitecture 27 | def os = DefaultNativePlatform.currentOperatingSystem 28 | 29 | def profile = ext.find('profile') ?: buildProfile ?: '' 30 | def buildTargetList = profile.tokenize(',') 31 | if (buildTargetList.contains('default')) { 32 | if (os.isWindows()) { 33 | useTarget['mingw'] = true 34 | logger.lifecycle "Detected ${os} using mingw" 35 | } 36 | if (os.isLinux()) { 37 | useTarget['linux'] = true 38 | logger.lifecycle "Detected ${org.gradle.internal.os.OperatingSystem.current()} using linux" 39 | } 40 | if (os.isMacOsX()) { 41 | useTarget['macos'] = true 42 | logger.lifecycle "Detected ${org.gradle.internal.os.OperatingSystem.current()} using macos" 43 | } 44 | } 45 | 46 | targetList.forEach { target -> 47 | if (!useTarget[target]) { 48 | useTarget[target] = buildTargetList.contains('all') || buildTargetList.contains(target) 49 | } 50 | if (buildTargetList.contains("-$target")) { 51 | useTarget[target] = false 52 | } 53 | } 54 | 55 | static def configureNative(srcSetMain, srcSetTest) { 56 | srcSetMain.kotlin.srcDirs = ['src/nativeMain/kotlin'] 57 | srcSetTest.kotlin.srcDirs = ['src/nativeTest/kotlin'] 58 | } 59 | 60 | 61 | logger.lifecycle(":platforms from: ${os.displayName} on ${arch.displayName}:${buildTargetList}=${useTarget}") 62 | kotlin { 63 | 64 | if (useTarget['jvm']) { 65 | jvm() { 66 | mavenPublication { 67 | artifactId = "${project.name}-jvm" 68 | } 69 | } 70 | } 71 | if (useTarget['js']) { 72 | js(IR) { 73 | mavenPublication { 74 | artifactId = "${project.name}-js" 75 | } 76 | nodejs() 77 | browser { 78 | testTask { 79 | useKarma { 80 | // useFirefox() 81 | useChromeHeadless() 82 | } 83 | } 84 | } 85 | compilations.main { 86 | kotlinOptions { 87 | metaInfo = true 88 | sourceMap = true 89 | verbose = true 90 | moduleKind = "umd" 91 | } 92 | } 93 | } 94 | } 95 | if (useTarget['mingw']) { 96 | mingwX64 { 97 | mavenPublication { 98 | artifactId = "${project.name}-mingw64" 99 | } 100 | } 101 | } 102 | if (useTarget['linux']) { 103 | if (arch.arm64) { 104 | linuxArm64() { 105 | mavenPublication { 106 | artifactId = "${project.name}-linuxarm64" 107 | } 108 | } 109 | } else if (arch.amd64) { 110 | linuxX64() { 111 | mavenPublication { 112 | artifactId = "${project.name}-linuxx64" 113 | } 114 | } 115 | } else { 116 | logger.error("Cannot use linux on:${arch.name}:${arch.displayName}") 117 | } 118 | } 119 | if (useTarget['macos']) { 120 | iosArm64() { 121 | mavenPublication { 122 | artifactId = "${project.name}-iosarm64" 123 | } 124 | } 125 | macosArm64() { 126 | mavenPublication { 127 | artifactId = "${project.name}-macosarm64" 128 | } 129 | } 130 | watchosArm64() { 131 | mavenPublication { 132 | artifactId = "${project.name}-watcharm64" 133 | } 134 | } 135 | tvosArm64() { 136 | mavenPublication { 137 | artifactId = "${project.name}-tvosarm64" 138 | } 139 | } 140 | } 141 | if (useTarget['wasm-js']) { 142 | wasmJs { 143 | mavenPublication { 144 | artifactId = "${project.name}-wasm-js" 145 | } 146 | browser { 147 | testTask { 148 | useKarma { 149 | // useFirefox() 150 | useChromeHeadless() 151 | } 152 | } 153 | } 154 | } 155 | } 156 | sourceSets { 157 | commonMain { 158 | dependencies { 159 | implementation kotlin('stdlib') 160 | } 161 | } 162 | commonTest { 163 | dependencies { 164 | implementation kotlin('test') 165 | implementation kotlin('test-annotations-common') 166 | } 167 | } 168 | if (useTarget['linux']) { 169 | if (arch.arm64) { 170 | configureNative(linuxArm64Main, linuxArm64Test) 171 | } else if (arch.amd64) { 172 | configureNative(linuxX64Main, linuxX64Test) 173 | } 174 | } 175 | if (useTarget['mingw']) { 176 | configureNative(mingwX64Main, mingwX64Test) 177 | } 178 | if (useTarget['macos']) { 179 | configureNative(macosArm64Main, macosArm64Test) 180 | configureNative(macosX64Main, macosX64Test) 181 | } 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /gradle/publish.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | apply plugin: 'signing' 20 | 21 | def useTarget = project.ext.useTarget 22 | 23 | task javadocJar(type: Jar) { 24 | archivesBaseName = 'kfsm' 25 | archiveClassifier = 'javadoc' 26 | from dokkaHtml.outputDirectory 27 | } 28 | 29 | 30 | gradle.afterProject { 31 | tasks.forEach { task -> 32 | if (task.name.contains('dokka')) { 33 | javadocJar.dependsOn(task) 34 | } 35 | if (task.name.contains('Publication')) { 36 | publish.dependsOn(task) 37 | } 38 | } 39 | } 40 | 41 | 42 | artifacts { 43 | archives javadocJar 44 | } 45 | 46 | def pomConfig = { 47 | licenses { 48 | license { 49 | name 'Apache License, Version 2.0' 50 | url 'http://www.apache.org/licenses/LICENSE-2.0' 51 | distribution 'repo' 52 | } 53 | } 54 | developers { 55 | developer { 56 | id 'corneil_jumpco' 57 | name 'Corneil @ JumpCO' 58 | organization 'Open JumpCO' 59 | organizationUrl 'https://open.jumpco.io' 60 | } 61 | } 62 | 63 | scm { 64 | url project.vcs 65 | } 66 | } 67 | 68 | def configureMavenCentralMetadata = { pom -> 69 | def root = asNode() 70 | root.appendNode('name', project.name) 71 | root.appendNode('description', project.description) 72 | root.appendNode('url', project.vcs) 73 | root.children().last() + pomConfig 74 | } 75 | 76 | publishing { 77 | repositories { 78 | maven { 79 | logger.lifecycle("maven:publish:version=$version") 80 | def snapshotsRepoUrl = 'https://oss.sonatype.org/content/repositories/snapshots' 81 | def releasesRepoUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2' 82 | url = project.version.endsWith('SNAPSHOT') ? snapshotsRepoUrl : releasesRepoUrl 83 | credentials { 84 | username rootProject.findProperty('ossJiraUser') ?: System.getenv('OSS_JIRA_USER') 85 | password rootProject.findProperty('ossJiraPwd') ?: System.getenv('OSS_JIRA_PWD') 86 | } 87 | } 88 | } 89 | def platforms = ['js', 'jvm', 'linux', 'mingw', 'macos', 'wasm-js'] 90 | publications.all { publication -> 91 | pom.withXml(configureMavenCentralMetadata) 92 | logger.info "publication:$publication.name" 93 | if (platforms.contains(publication.name)) { 94 | publication.artifact javadocJar 95 | } 96 | } 97 | } 98 | 99 | def skipSign = hasProperty('skipSign') ?: 'false' 100 | if (!skipSign.asBoolean()) { 101 | signing { 102 | sign publishing.publications.kotlinMultiplatform 103 | // sign publishing.publications.metadata 104 | sign publishing.publications.documentation 105 | publishing.publications.forEach { 106 | if (it.name != publishing.publications.kotlinMultiplatform.name && it.name != publishing.publications.documentation.name) { 107 | logger.info("sign:$it.name") 108 | sign it 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-all.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /kfsm-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/kfsm-fsm.png -------------------------------------------------------------------------------- /packet_reader.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/packet_reader.png -------------------------------------------------------------------------------- /paying_turnstile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/paying_turnstile.png -------------------------------------------------------------------------------- /publish-linux.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | ./gradlew -i --continue publishLinuxPublicationToMavenRepository -x dokkaJavadoc -x dokkaGfm -x dokkaJekyll 3 | -------------------------------------------------------------------------------- /publish.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | gradlew -S -i build publishAllPublicationsToMavenRepository publishDocumentationPublicationToMavenRepository -x dokkaJavadoc -x dokkaGfm -x dokkaJekyll 3 | -------------------------------------------------------------------------------- /publish.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | ./gradlew -S -i build publishAllPublicationsToMavenRepository publishDocumentationPublicationToMavenRepository -x dokkaJavadoc -x dokkaGfm -x dokkaJekyll 3 | -------------------------------------------------------------------------------- /qodana.cmd: -------------------------------------------------------------------------------- 1 | docker run --rm -it -p 8080:8080 -v src/:/data/project/ -v build/qodana/:/data/results/ jetbrains/qodana-jvm --show-report 2 | -------------------------------------------------------------------------------- /secure_turnstile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/secure_turnstile.png -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | mavenLocal() 4 | maven { url = 'https://maven.pkg.jetbrains.space/kotlin/p/wasm/experimental' } 5 | gradlePluginPortal() 6 | mavenCentral() 7 | } 8 | } 9 | 10 | //plugins { 11 | // id "com.gradle.enterprise" version "3.16.2" 12 | //} 13 | // 14 | //gradleEnterprise { 15 | //} 16 | 17 | rootProject.name = 'kfsm' 18 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/DefaultSyncTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * @author Corneil du Plessis 23 | * @soundtrack Wolfgang Amadeus Mozart 24 | * @suppress 25 | * Represents a DefaultTransition 26 | * @param event The event identifies the transition 27 | * @param targetState when optional represents an internal transition 28 | * @param action optional lambda will be invoked when transition occurs. 29 | */ 30 | class DefaultSyncTransition( 31 | internal val event: E, 32 | targetState: S?, 33 | targetMap: String?, 34 | automatic: Boolean, 35 | type: TransitionType, 36 | action: SyncStateAction? 37 | ) : SyncTransition(targetState, targetMap, automatic, type, action) 38 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/DslStateMachineHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * This handler will be active inside the top level of the stateMachine definition. 23 | * @author Corneil du Plessis 24 | * @soundtrack Wolfgang Amadeus Mozart 25 | */ 26 | class DslStateMachineHandler(private val fsm: StateMachineBuilder) { 27 | /** 28 | * Defines an expression that will determine the initial state of the state machine based on the values of the context. 29 | * @param deriveInitialState A lambda expression receiving context:C and returning state S. 30 | */ 31 | fun initialState(deriveInitialState: StateQuery): DslStateMachineHandler { 32 | fsm.initialState(deriveInitialState) 33 | return this 34 | } 35 | 36 | var defaultInitialState: S? 37 | get() = fsm.defaultInitialState 38 | set(state) { 39 | fsm.defaultInitialState = state 40 | } 41 | 42 | /** 43 | * Provides for a list of pairs with state and map name that will be pushed and the last entry will be popped and become the current map. 44 | * This is required when using state machine with named maps. 45 | * 46 | 47 | ``` 48 | initialStates { 49 | mutableListOf>().apply { 50 | if (locked) { 51 | this.add(PayingTurnstileStates.LOCKED to "default") 52 | } else { 53 | this.add(PayingTurnstileStates.UNLOCKED to "default") 54 | } 55 | if (coins > 0) { 56 | this.add(PayingTurnstileStates.COINS to "coins") 57 | } 58 | } 59 | } 60 | ``` 61 | 62 | */ 63 | fun initialStates(deriveInitialMap: StateMapQuery): DslStateMachineHandler { 64 | fsm.initialStates(deriveInitialMap) 65 | return this 66 | } 67 | 68 | fun invariant(message: String, condition: Condition): DslStateMachineHandler { 69 | fsm.invariant(message, condition) 70 | return this 71 | } 72 | 73 | /** 74 | * Defines an action that will be invoked after a transition to a new state. 75 | * Any exceptions thrown by the action will be ignored. 76 | */ 77 | fun onStateChange(action: StateChangeAction) { 78 | fsm.afterStateChange(action) 79 | } 80 | 81 | /** 82 | * Defines a section for a specific state. 83 | * @param currentState The give state 84 | * @param handler A lambda with definitions for the given state 85 | */ 86 | fun whenState(currentState: S, handler: DslStateMapEventHandler.() -> Unit): 87 | DslStateMapEventHandler = 88 | DslStateMapEventHandler(currentState, fsm.defaultStateMap).apply(handler) 89 | 90 | /** 91 | * Defines a section for default behaviour for the state machine. 92 | * @param handler A lambda with definition for the default behaviour of the state machine. 93 | */ 94 | fun default(handler: DslStateMapDefaultEventHandler.() -> Unit): 95 | DslStateMapDefaultEventHandler = 96 | DslStateMapDefaultEventHandler(fsm.defaultStateMap).apply(handler) 97 | 98 | /** 99 | * Returns the completed fsm. 100 | */ 101 | fun build() = fsm.complete() 102 | 103 | /** 104 | * creates a named statemap 105 | */ 106 | fun stateMap( 107 | /** 108 | * The name of the state map. , 115 | /** 116 | * The lambda to configure the statemap 117 | */ 118 | handler: DslStateMapHandler.() -> Unit 119 | ): DslStateMapHandler { 120 | return DslStateMapHandler(fsm.stateMap(name.trim(), validStates)).apply(handler) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/DslStateMapDefaultEventHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * This handler will be active inside the default section of the statemachine. 23 | * @author Corneil du Plessis 24 | * @soundtrack Wolfgang Amadeus Mozart 25 | */ 26 | class DslStateMapDefaultEventHandler(private val fsm: StateMapBuilder) { 27 | /** 28 | * Define a default action that will be applied when no other transitions are matched. 29 | * @param action Will be invoked when no transitions match 30 | */ 31 | fun action(action: DefaultStateAction) { 32 | fsm.defaultAction(action) 33 | } 34 | 35 | /** 36 | * Defines an action to perform before a change in the currentState of the FSM 37 | * @param action This action will be performed when entering a new state. 38 | */ 39 | fun onEntry(action: DefaultEntryExitAction) { 40 | fsm.defaultEntry(action) 41 | } 42 | 43 | /** 44 | * Defines an action to be performed after the currentState was changed. 45 | * @param action The action will be performed when leaving any state. 46 | */ 47 | fun onExit(action: DefaultEntryExitAction) { 48 | fsm.defaultExit(action) 49 | } 50 | 51 | /** 52 | * Defines a default transition when an on is received to a specific state. 53 | * @param event Pair representing an on and targetState for transition. Can be written as EVENT to STATE 54 | * @param action The action will be performed before transition is completed 55 | */ 56 | fun onEvent( 57 | event: EventState, 58 | action: SyncStateAction? 59 | ): DslStateMapDefaultEventHandler { 60 | fsm.default(event, action) 61 | return this 62 | } 63 | 64 | /** 65 | * Defines a default internal transition for a specific event with no change in state. 66 | * @param event The event that triggers this transition 67 | * @param action The action will be invoked for this transition 68 | */ 69 | fun onEvent(event: E, action: SyncStateAction?): DslStateMapDefaultEventHandler { 70 | fsm.default(event, action) 71 | return this 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/DslStateMapHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * This handler will be active inside the top level of the stateMachine definition. 23 | * @author Corneil du Plessis 24 | * @soundtrack Wolfgang Amadeus Mozart 25 | */ 26 | class DslStateMapHandler(private val fsm: StateMapBuilder) { 27 | 28 | /** 29 | * Defines a section for a specific state. 30 | * @param currentState The give state 31 | * @param handler A lambda with definitions for the given state 32 | */ 33 | fun whenState(currentState: S, handler: DslStateMapEventHandler.() -> Unit): 34 | DslStateMapEventHandler = 35 | DslStateMapEventHandler(currentState, fsm).apply(handler) 36 | 37 | /** 38 | * Defines a section for default behaviour for the state machine. 39 | * @param handler A lambda with definition for the default behaviour of the state machine. 40 | */ 41 | fun default(handler: DslStateMapDefaultEventHandler.() -> Unit): 42 | DslStateMapDefaultEventHandler = 43 | DslStateMapDefaultEventHandler(fsm).apply(handler) 44 | } 45 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/SimpleSyncTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * @author Corneil du Plessis 23 | * @soundtrack Wolfgang Amadeus Mozart 24 | * @suppress 25 | * Represents a transition from a given state and event. 26 | * @param startState The given state 27 | * @param event The given event 28 | * @param targetState when optional represents an internal transition 29 | * @param action An optional lambda that will be invoked. 30 | */ 31 | open class SimpleSyncTransition( 32 | internal val startState: S, 33 | internal val event: E?, 34 | targetState: S?, 35 | targetMap: String?, 36 | automatic: Boolean, 37 | type: TransitionType, 38 | action: SyncStateAction? 39 | ) : SyncTransition(targetState, targetMap, automatic, type, action) 40 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/StateMachineDefinition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * This class represents an immutable definition of a state machine. 23 | * @author Corneil du Plessis 24 | * @soundtrack Wolfgang Amadeus Mozart 25 | */ 26 | class StateMachineDefinition( 27 | val defaultInitialState: S?, 28 | private val deriveInitialState: StateQuery?, 29 | private val deriveInitialMap: StateMapQuery?, 30 | /** 31 | * The top level state map will be created when the state machine is created. 32 | */ 33 | val defaultStateMap: StateMapDefinition, 34 | /** 35 | * The named state maps can be accessed via a push transition. 36 | */ 37 | val namedStateMaps: Map> 38 | ) { 39 | private fun createMap( 40 | mapName: String, 41 | context: C, 42 | parentFsm: StateMachineInstance, 43 | initial: S 44 | ) { 45 | if (mapName == "default") { 46 | parentFsm.pushMap(StateMapInstance(context, initial, null, parentFsm, defaultStateMap)) 47 | } else { 48 | val stateMap = namedStateMaps[mapName] ?: error("Invalid map $mapName") 49 | parentFsm.pushMap(StateMapInstance(context, initial, mapName, parentFsm, stateMap)) 50 | } 51 | } 52 | 53 | /** 54 | * This function will create a state machine instance provided with content and optional initialState. 55 | * @param context The context will be provided to actions 56 | * @param initialState If this is not provided the function defined in `initial` will be invoked to derive the initialState. 57 | * @see StateMachineBuilder.initialState 58 | */ 59 | internal fun create( 60 | context: C, 61 | parentFsm: StateMachineInstance, 62 | initialState: S? = null, 63 | intitialExternalState: ExternalState? = null 64 | ): StateMapInstance { 65 | return when { 66 | intitialExternalState != null -> { 67 | intitialExternalState.forEach { (initial, mapName) -> 68 | createMap(mapName, context, parentFsm, initial) 69 | } 70 | parentFsm.mapStack.pop() 71 | } 72 | 73 | deriveInitialMap != null -> { 74 | deriveInitialMap.invoke(context).forEach { (initial, mapName) -> 75 | createMap(mapName, context, parentFsm, initial) 76 | } 77 | parentFsm.mapStack.pop() 78 | } 79 | 80 | else -> { 81 | val initial = initialState ?: deriveInitialState?.invoke(context) ?: defaultInitialState 82 | ?: error("Definition requires deriveInitialState or deriveInitialMap") 83 | StateMapInstance(context, initial, null, parentFsm, defaultStateMap) 84 | } 85 | } 86 | } 87 | 88 | internal fun createStateMap( 89 | name: String, 90 | context: C, 91 | parentFsm: StateMachineInstance, 92 | initialState: S 93 | ): StateMapInstance = 94 | StateMapInstance( 95 | context, 96 | initialState, 97 | name, 98 | parentFsm, 99 | namedStateMaps[name] ?: error("Named map $name not found") 100 | ) 101 | 102 | /** 103 | * This function will create a state machine instance and set it to the state to a previously externalised state. 104 | * @param context The instance will operate on the provided context 105 | * @param initialExternalState The previously externalised state 106 | */ 107 | fun create(context: C, initialExternalState: ExternalState): StateMachineInstance = 108 | StateMachineInstance(context, this, null, initialExternalState) 109 | 110 | /** 111 | * This function will create a state machine instance and set it to the initial state. 112 | * @param context The instance will operate on the provided context 113 | * @param initial The initial state 114 | * 115 | */ 116 | fun create(context: C, initial: S? = null): StateMachineInstance = 117 | StateMachineInstance(context, this, initial) 118 | 119 | /** 120 | * This function will provide a list of possible events given a specific state. 121 | * The actual events may fail because of guard conditions or named state maps and the default state map behaviour being different. 122 | * @param state The given state 123 | * @param includeDefault consider the default state and event handlers 124 | */ 125 | fun possibleEvents(state: S, includeDefault: Boolean): Set { 126 | val result = mutableSetOf() 127 | result.addAll(defaultStateMap.allowed(state, includeDefault)) 128 | namedStateMaps.values.forEach { 129 | if (it.validStates.contains(state)) { 130 | result.addAll(it.allowed(state, includeDefault)) 131 | } 132 | } 133 | return result.toSet() 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/StateMapDefinition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * Contains the definition of a state map. A state machine has at least one top-level state map. 23 | * @author Corneil du Plessis 24 | * @soundtrack Wolfgang Amadeus Mozart 25 | */ 26 | class StateMapDefinition( 27 | /** 28 | * The name of the statemap. The top-level state map name is `null` 29 | */ 30 | val name: String?, 31 | /** 32 | * A set of the valid states for this map. 33 | */ 34 | val validStates: Set, 35 | /** 36 | * Invariant conditions are checked before and after every transition an will throw an InvariantException if false 37 | */ 38 | val invariants: Set>>, 39 | /** 40 | * transitionRule contains a map of TransitionRules that is keyed by a Pair of state,event 41 | * This will be the most common transition rule. 42 | */ 43 | val transitionRules: Map, SyncTransitionRules>, 44 | /** 45 | * The default transitions will be used if no transition of found matching a given event 46 | */ 47 | val defaultTransitions: Map>, 48 | /** 49 | * This is a map of actions keyed by the state. A specific action will be invoked when a state is entered. 50 | */ 51 | val entryActions: Map>, 52 | /** 53 | * This is a map of actions keyed by the state. A specific action will be invoked when a state is exited. 54 | */ 55 | val exitActions: Map>, 56 | /** 57 | * This is a map of default actions for event on specific startState. 58 | */ 59 | val defaultActions: Map>, 60 | /** 61 | * This a map of TransitionRules by state for automatic transitions. 62 | */ 63 | val automaticTransitions: Map>, 64 | /** 65 | * This is the action that will be invoked of no other has been matched 66 | */ 67 | val globalDefault: DefaultStateAction?, 68 | /** 69 | * This is the default action that will be invoked when entering any state when no other action has been matched. 70 | */ 71 | val defaultEntryAction: DefaultEntryExitAction?, 72 | /** 73 | * This is the default action that will be invoked when exiting any state when no other action has been matched. 74 | */ 75 | val defaultExitAction: DefaultEntryExitAction?, 76 | /** 77 | * This action will be invoked after a change in the state of the statemachine. 78 | * This machine will catch and ignore any exceptions thrown by the handler. 79 | */ 80 | val afterStateChangeAction: StateChangeAction? 81 | ) { 82 | /** 83 | * This function will provide the set of allowed events given a specific state. It isn't a guarantee that a 84 | * subsequent transition will be successful since a guard may prevent a transition. Default state handlers are not considered. 85 | * @param given The specific state to consider 86 | * @param includeDefault When `true` will include default transitions in the list of allowed events. 87 | */ 88 | fun allowed(given: S, includeDefault: Boolean = false): Set { 89 | val result = transitionRules.entries.filter { 90 | it.key.first == given 91 | }.map { 92 | it.key.second 93 | }.toSet() 94 | if (includeDefault && defaultTransitions.isNotEmpty()) { 95 | return result + defaultTransitions.keys 96 | } 97 | return result 98 | } 99 | 100 | /** 101 | * This function will provide an indicator if an event is allow for a given state. 102 | * When no state transition is declared this function will return false unless `includeDefault` is true and 103 | * there is a default transition of handler for the event. 104 | */ 105 | fun eventAllowed(event: E, given: S, includeDefault: Boolean): Boolean = 106 | (includeDefault && hasDefaultStateHandler(given)) || allowed(given, includeDefault).contains(event) 107 | 108 | /** 109 | * This function will provide an indicator if a default action has been defined for a given state. 110 | */ 111 | private fun hasDefaultStateHandler(given: S) = defaultActions.contains(given) 112 | } 113 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/SyncGuardedTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * @suppress 23 | * Represents a guarded transition. The transition will be considered if the guard expression is true 24 | * @author Corneil du Plessis 25 | * @soundtrack Wolfgang Amadeus Mozart 26 | * @param startState The given state 27 | * @param event The given event 28 | * @param targetState when optional represents an internal transition 29 | * @param guard Expression lambda returning a Boolean 30 | * @param action An optional lambda that will be invoked. 31 | */ 32 | open class SyncGuardedTransition( 33 | startState: S, 34 | event: E?, 35 | targetState: S?, 36 | targetMap: String?, 37 | automatic: Boolean, 38 | type: TransitionType, 39 | val guard: StateGuard, 40 | action: SyncStateAction? 41 | ) : SimpleSyncTransition(startState, event, targetState, targetMap, automatic, type, action) { 42 | /** 43 | * This function will invoke the guard expression using the provided context to determine if transition can be considered. 44 | * @param context The provided context 45 | * @return result of guard lambda 46 | */ 47 | fun guardMet(context: C, arg: A?): Boolean = guard.invoke(context, arg) 48 | } 49 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/SyncTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * @suppress 23 | * The base for all transitions. 24 | * @author Corneil du Plessis 25 | * @soundtrack Wolfgang Amadeus Mozart 26 | * @param targetState when optional represents an internal transition 27 | * @param action optional lambda will be invoked when transition occurs. 28 | */ 29 | open class SyncTransition( 30 | val targetState: S? = null, 31 | val targetMap: String? = null, 32 | val automatic: Boolean = false, 33 | val type: TransitionType = TransitionType.NORMAL, 34 | val action: SyncStateAction? = null 35 | ) { 36 | init { 37 | if (type == TransitionType.PUSH) { 38 | require(targetState != null) { "targetState is required for push transition" } 39 | } 40 | } 41 | 42 | /** 43 | * Executed exit, optional and entry actions specific in the transition. 44 | */ 45 | open fun execute(context: C, instance: StateMapInstance, arg: A?): R? { 46 | 47 | if (isExternal()) { 48 | instance.executeExit(context, targetState!!, arg) 49 | } 50 | val result = action?.invoke(context, arg) 51 | if (isExternal()) { 52 | instance.executeEntry(context, targetState!!, arg) 53 | } 54 | return result 55 | } 56 | 57 | /** 58 | * Executed exit, optional and entry actions specific in the transition. 59 | */ 60 | open fun execute( 61 | context: C, 62 | sourceMap: StateMapInstance, 63 | targetMap: StateMapInstance?, 64 | arg: A? 65 | ): R? { 66 | 67 | if (isExternal()) { 68 | sourceMap.executeExit(context, targetState!!, arg) 69 | } 70 | val result = action?.invoke(context, arg) 71 | if (targetMap != null && isExternal()) { 72 | targetMap.executeEntry(context, targetState!!, arg) 73 | } 74 | return result 75 | } 76 | 77 | /** 78 | * This function provides an indicator if a Transition is internal or external. 79 | * When there is no targetState defined a Transition is considered internal and will not trigger entry or exit actions. 80 | */ 81 | fun isExternal(): Boolean = targetState != null 82 | } 83 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/SyncTransitionRules.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | /** 22 | * @suppress 23 | * Represents a collection of rule with 1 transition and a list of guarded transitions. 24 | * @author Corneil du Plessis 25 | * @soundtrack Wolfgang Amadeus Mozart 26 | * @param guardedTransitions The list of guarded transitions 27 | * @param transition The transition to use if there are no guarded transitions or no guarded transitions match. 28 | */ 29 | class SyncTransitionRules( 30 | val guardedTransitions: MutableList> = mutableListOf(), 31 | transition: SimpleSyncTransition? = null 32 | ) { 33 | var transition: SimpleSyncTransition? = transition 34 | internal set 35 | 36 | /** 37 | * Add a guarded transition to the end of the list 38 | */ 39 | fun addGuarded(guardedTransition: SyncGuardedTransition) { 40 | guardedTransitions.add(guardedTransition) 41 | } 42 | 43 | /** 44 | * Find the first entry in the list of guarded transitions that match/ 45 | * @param context The given context. 46 | */ 47 | fun findGuard(context: C, arg: A? = null): SyncGuardedTransition? = 48 | guardedTransitions.firstOrNull { it.guardMet(context, arg) } 49 | } 50 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncDslStateMachineHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateChangeAction 22 | import io.jumpco.open.kfsm.StateMapQuery 23 | import io.jumpco.open.kfsm.StateQuery 24 | 25 | class AsyncDslStateMachineHandler(private val fsm: AsyncStateMachineBuilder) { 26 | /** 27 | * Defines an expression that will determine the initial state of the state machine based on the values of the context. 28 | * @param deriveInitialState A lambda expression receiving context:C and returning state S. 29 | */ 30 | fun initialState(deriveInitialState: StateQuery): AsyncDslStateMachineHandler { 31 | fsm.initialState(deriveInitialState) 32 | return this 33 | } 34 | 35 | var defaultInitialState: S? 36 | get() = fsm.defaultInitialState 37 | set(state) { 38 | fsm.defaultInitialState = state 39 | } 40 | 41 | /** 42 | * Provides for a list of pairs with state and map name that will be pushed and the last entry will be popped and become the current map. 43 | * This is required when using state machine with named maps. 44 | * 45 | ``` 46 | initialStates { 47 | mutableListOf>().apply { 48 | if (locked) { 49 | this.add(PayingTurnstileStates.LOCKED to "default") 50 | } else { 51 | this.add(PayingTurnstileStates.UNLOCKED to "default") 52 | } 53 | if (coins > 0) { 54 | this.add(PayingTurnstileStates.COINS to "coins") 55 | } 56 | } 57 | } 58 | ``` 59 | */ 60 | fun initialStates(deriveInitialMap: StateMapQuery): AsyncDslStateMachineHandler { 61 | fsm.initialStates(deriveInitialMap) 62 | return this 63 | } 64 | 65 | /** 66 | * Defines an action that will be invoked after a transition to a new state. 67 | * Any exceptions thrown by the action will be ignored. 68 | */ 69 | fun onStateChange(action: AsyncStateChangeAction) { 70 | fsm.afterStateChange(action) 71 | } 72 | 73 | /** 74 | * Defines a section for a specific state. 75 | * @param currentState The give state 76 | * @param handler A lambda with definitions for the given state 77 | */ 78 | fun whenState(currentState: S, handler: AsyncDslStateMapEventHandler.() -> Unit): 79 | AsyncDslStateMapEventHandler = 80 | AsyncDslStateMapEventHandler(currentState, fsm.defaultStateMap).apply(handler) 81 | 82 | /** 83 | * Defines a section for default behaviour for the state machine. 84 | * @param handler A lambda with definition for the default behaviour of the state machine. 85 | */ 86 | fun default(handler: AsyncDslStateMapDefaultEventHandler.() -> Unit): 87 | AsyncDslStateMapDefaultEventHandler = 88 | AsyncDslStateMapDefaultEventHandler(fsm.defaultStateMap).apply(handler) 89 | 90 | /** 91 | * Returns the completed fsm. 92 | */ 93 | fun build() = fsm.complete() 94 | 95 | /** 96 | * creates a named statemap 97 | */ 98 | fun stateMap( 99 | /** 100 | * The name of the state map. , 107 | /** 108 | * The lambda to configure the statemap 109 | */ 110 | handler: AsyncDslStateMapHandler.() -> Unit 111 | ): AsyncDslStateMapHandler { 112 | return AsyncDslStateMapHandler( 113 | fsm.stateMap( 114 | name.trim(), 115 | validStates 116 | ) 117 | ).apply(handler) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncDslStateMapDefaultEventHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateAction 22 | import io.jumpco.open.kfsm.DefaultAsyncStateAction 23 | import io.jumpco.open.kfsm.DefaultEntryExitAction 24 | import io.jumpco.open.kfsm.EventState 25 | 26 | class AsyncDslStateMapDefaultEventHandler(private val fsm: AsyncStateMapBuilder) { 27 | /** 28 | * Define a default action that will be applied when no other transitions are matched. 29 | * @param action Will be invoked when no transitions matches 30 | */ 31 | fun action(action: DefaultAsyncStateAction) { 32 | fsm.defaultAction(action) 33 | } 34 | 35 | /** 36 | * Defines an action to perform before a change in the currentState of the FSM 37 | * @param action This action will be performed when entering a new state. 38 | */ 39 | fun onEntry(action: DefaultEntryExitAction) { 40 | fsm.defaultEntry(action) 41 | } 42 | 43 | /** 44 | * Defines an action to be performed after the currentState was changed. 45 | * @param action The action will be performed when leaving any state. 46 | */ 47 | fun onExit(action: DefaultEntryExitAction) { 48 | fsm.defaultExit(action) 49 | } 50 | 51 | /** 52 | * Defines a default transition when an on is received to a specific state. 53 | * @param event Pair representing an on and targetState for transition. Can be written as EVENT to STATE 54 | * @param action The action will be performed before transition is completed 55 | */ 56 | fun onEvent( 57 | event: EventState, 58 | action: AsyncStateAction? 59 | ): AsyncDslStateMapDefaultEventHandler { 60 | fsm.default(event, action) 61 | return this 62 | } 63 | 64 | /** 65 | * Defines a default internal transition for a specific event with no change in state. 66 | * @param event The event that triggers this transition 67 | * @param action The action will be invoked for this transition 68 | */ 69 | fun onEvent(event: E, action: AsyncStateAction?): AsyncDslStateMapDefaultEventHandler { 70 | fsm.default(event, action) 71 | return this 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncDslStateMapHandler.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | /** 22 | * This handler will be active inside the top level of the stateMachine definition. 23 | */ 24 | class AsyncDslStateMapHandler(private val fsm: AsyncStateMapBuilder) { 25 | 26 | /** 27 | * Defines a section for a specific state. 28 | * @param currentState The give state 29 | * @param handler A lambda with definitions for the given state 30 | */ 31 | fun whenState(currentState: S, handler: AsyncDslStateMapEventHandler.() -> Unit): 32 | AsyncDslStateMapEventHandler = 33 | AsyncDslStateMapEventHandler(currentState, fsm).apply(handler) 34 | 35 | /** 36 | * Defines a section for default behaviour for the state machine. 37 | * @param handler A lambda with definition for the default behaviour of the state machine. 38 | */ 39 | fun default(handler: AsyncDslStateMapDefaultEventHandler.() -> Unit): 40 | AsyncDslStateMapDefaultEventHandler = 41 | AsyncDslStateMapDefaultEventHandler(fsm).apply(handler) 42 | } 43 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncGuardedTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateAction 22 | import io.jumpco.open.kfsm.StateGuard 23 | import io.jumpco.open.kfsm.TransitionType 24 | 25 | open class AsyncGuardedTransition( 26 | startState: S, 27 | event: E?, 28 | targetState: S?, 29 | targetMap: String?, 30 | automatic: Boolean, 31 | type: TransitionType, 32 | val guard: StateGuard, 33 | action: AsyncStateAction? 34 | ) : SimpleAsyncTransition(startState, event, targetState, targetMap, automatic, type, action) { 35 | /** 36 | * This function will invoke the guard expression using the provided context to determine if transition can be considered. 37 | * @param context The provided context 38 | * @return result of guard lambda 39 | */ 40 | fun guardMet(context: C, arg: A?): Boolean = guard.invoke(context, arg) 41 | } 42 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncStateMapDefinition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateChangeAction 22 | import io.jumpco.open.kfsm.Condition 23 | import io.jumpco.open.kfsm.DefaultAsyncStateAction 24 | import io.jumpco.open.kfsm.DefaultEntryExitAction 25 | 26 | class AsyncStateMapDefinition( 27 | /** 28 | * The name of the statemap. The top-level state map name is `null` 29 | */ 30 | val name: String?, 31 | /** 32 | * A set of the valid states for this map. 33 | */ 34 | val validStates: Set, 35 | /** 36 | * Invariant conditions are checked before and after every transition and will throw an InvariantException if false 37 | */ 38 | val invariants: Set>>, 39 | /** 40 | * transitionRule contains a map of TransitionRules that is keyed by a Pair of state,event 41 | * This will be the most common transition rule. 42 | */ 43 | val transitionRules: Map, AsyncTransitionRules>, 44 | /** 45 | * The default transitions will be used if no transition of found matching a given event 46 | */ 47 | val defaultTransitions: Map>, 48 | /** 49 | * This is a map of actions keyed by the state. A specific action will be invoked when a state is entered. 50 | */ 51 | val entryActions: Map>, 52 | /** 53 | * This is a map of actions keyed by the state. A specific action will be invoked when a state is exited. 54 | */ 55 | val exitActions: Map>, 56 | /** 57 | * This is a map of default actions for event on specific startState. 58 | */ 59 | val defaultActions: Map>, 60 | /** 61 | * This a map of TransitionRules by state for automatic transitions. 62 | */ 63 | val automaticTransitions: Map>, 64 | /** 65 | * The timer definitions will be activated on entry to state and deactivated on state exit 66 | */ 67 | val timerDefinitions: Map>, 68 | /** 69 | * This is the action that will be invoked of no other has been matched 70 | */ 71 | val globalDefault: DefaultAsyncStateAction?, 72 | /** 73 | * This is the default action that will be invoked when entering any state when no other action has been matched. 74 | */ 75 | val defaultEntryAction: DefaultEntryExitAction?, 76 | /** 77 | * This is the default action that will be invoked when exiting any state when no other action has been matched. 78 | */ 79 | val defaultExitAction: DefaultEntryExitAction?, 80 | /** 81 | * This action will be invoked after a change in the state of the statemachine. 82 | * This machine will catch and ignore any exceptions thrown by the handler. 83 | */ 84 | val afterStateChangeAction: AsyncStateChangeAction? 85 | ) { 86 | /** 87 | * This function will provide the set of allowed events given a specific state. It isn't a guarantee that a 88 | * subsequent transition will be successful since a guard may prevent a transition. Default state handlers are not considered. 89 | * @param given The specific state to consider 90 | * @param includeDefault When `true` will include default transitions in the list of allowed events. 91 | */ 92 | fun allowed(given: S, includeDefault: Boolean = false): Set { 93 | val result = transitionRules.entries.filter { 94 | it.key.first == given 95 | }.map { 96 | it.key.second 97 | }.toSet() 98 | if (includeDefault && defaultTransitions.isNotEmpty()) { 99 | return result + defaultTransitions.keys 100 | } 101 | return result 102 | } 103 | 104 | /** 105 | * This function will provide an indicator if an event is allowed for a given state. 106 | * When no state transition is declared this function will return false unless `includeDefault` is true and 107 | * there is a default transition of handler for the event. 108 | */ 109 | fun eventAllowed(event: E, given: S, includeDefault: Boolean): Boolean = 110 | (includeDefault && 111 | hasDefaultStateHandler(given)) || 112 | allowed(given, includeDefault).contains(event) 113 | 114 | /** 115 | * This function will provide an indicator if a default action has been defined for a given state. 116 | */ 117 | private fun hasDefaultStateHandler(given: S) = defaultActions.contains(given) 118 | } 119 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncTimer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import kotlinx.coroutines.CoroutineScope 22 | import kotlinx.coroutines.Job 23 | import kotlinx.coroutines.async 24 | import kotlinx.coroutines.delay 25 | import kotlinx.atomicfu.* 26 | class AsyncTimer constructor( 27 | private val parentFsm: AsyncStateMapInstance, 28 | val context: C, 29 | val arg: A?, 30 | val definition: AsyncTimerDefinition, 31 | coroutineScope: CoroutineScope 32 | ) { 33 | private val active = atomic(false) 34 | private val timer: Job 35 | 36 | init { 37 | active.value = true 38 | timer = coroutineScope.async { 39 | delay(context.(definition.timeout)()) 40 | trigger() 41 | } 42 | } 43 | 44 | fun cancel() { 45 | active.value = false 46 | } 47 | 48 | suspend fun trigger() { 49 | if (active.value) { 50 | val defaultTransition = definition.rule.transition 51 | definition.rule.findGuard(context, arg)?.apply { 52 | parentFsm.execute(this, arg) 53 | } ?: run { 54 | if (defaultTransition != null) { 55 | parentFsm.execute(defaultTransition, arg) 56 | } 57 | } 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncTimerDefinition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | class AsyncTimerDefinition( 22 | val timeout: C.() -> Long, 23 | val rule: AsyncTransitionRules 24 | ) 25 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateAction 22 | import io.jumpco.open.kfsm.TransitionType 23 | 24 | open class AsyncTransition( 25 | val targetState: S? = null, 26 | val targetMap: String? = null, 27 | val automatic: Boolean = false, 28 | val type: TransitionType = TransitionType.NORMAL, 29 | val action: AsyncStateAction? = null 30 | ) { 31 | init { 32 | if (type == TransitionType.PUSH) { 33 | require(targetState != null) { "targetState is required for push transition" } 34 | } 35 | } 36 | 37 | /** 38 | * Executed exit, optional and entry actions specific in the transition. 39 | */ 40 | open suspend fun execute(context: C, instance: AsyncStateMapInstance, arg: A?): R? { 41 | 42 | if (isExternal()) { 43 | instance.executeExit(context, targetState!!, arg) 44 | } 45 | val result = action?.invoke(context, arg) 46 | if (isExternal()) { 47 | instance.executeEntry(context, targetState!!, arg) 48 | } 49 | return result 50 | } 51 | 52 | /** 53 | * Executed exit, optional and entry actions specific in the transition. 54 | */ 55 | open suspend fun execute( 56 | context: C, 57 | sourceMap: AsyncStateMapInstance, 58 | targetMap: AsyncStateMapInstance?, 59 | arg: A? 60 | ): R? { 61 | if (isExternal()) { 62 | sourceMap.executeExit(context, targetState!!, arg) 63 | } 64 | val result = action?.invoke(context, arg) 65 | if (targetMap != null && isExternal()) { 66 | targetMap.executeEntry(context, targetState!!, arg) 67 | } 68 | return result 69 | } 70 | 71 | /** 72 | * This function provides an indicator if a Transition is internal or external. 73 | * When there is no targetState defined a Transition is considered internal and will not trigger entry or exit actions. 74 | */ 75 | fun isExternal(): Boolean = targetState != null 76 | } 77 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/AsyncTransitionRules.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | class AsyncTransitionRules( 22 | val guardedTransitions: MutableList> = mutableListOf(), 23 | transition: SimpleAsyncTransition? = null 24 | ) { 25 | var transition: SimpleAsyncTransition? = transition 26 | internal set 27 | 28 | /** 29 | * Add a guarded transition to the end of the list 30 | */ 31 | fun addGuarded(guardedTransition: AsyncGuardedTransition) { 32 | guardedTransitions.add(guardedTransition) 33 | } 34 | 35 | /** 36 | * Find the first entry in the list of guarded transitions that match/ 37 | * @param context The given context. 38 | */ 39 | fun findGuard(context: C, arg: A? = null): AsyncGuardedTransition? = 40 | guardedTransitions.firstOrNull { it.guardMet(context, arg) } 41 | } 42 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/DefaultAsyncTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateAction 22 | import io.jumpco.open.kfsm.TransitionType 23 | 24 | class DefaultAsyncTransition( 25 | internal val event: E, 26 | targetState: S?, 27 | targetMap: String?, 28 | automatic: Boolean, 29 | type: TransitionType, 30 | action: AsyncStateAction? 31 | ) : AsyncTransition(targetState, targetMap, automatic, type, action) 32 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/SimpleAsyncTransition.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import io.jumpco.open.kfsm.AsyncStateAction 22 | import io.jumpco.open.kfsm.TransitionType 23 | 24 | open class SimpleAsyncTransition( 25 | internal val startState: S, 26 | internal val event: E?, 27 | targetState: S?, 28 | targetMap: String?, 29 | automatic: Boolean, 30 | type: TransitionType, 31 | action: AsyncStateAction? 32 | ) : AsyncTransition(targetState, targetMap, automatic, type, action) 33 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/async/asyncStateMachines.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.async 20 | 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * These function are used to create state machines where the actions are suspend functions. 25 | */ 26 | inline fun asyncStateMachine( 27 | validStates: Set, 28 | validEvents: Set, 29 | contextClass: KClass, 30 | argumentClass: KClass, 31 | returnClass: KClass, 32 | handler: AsyncDslStateMachineHandler.() -> Unit 33 | ) = AsyncStateMachineBuilder(validStates, validEvents).stateMachine(handler) 34 | 35 | /** 36 | * Defines the start of a state machine DSL declaration with `Any` as the return type 37 | * @param validStates A set of the possible states supported by the top-level state map 38 | * @param validEvents The class of the possible events* 39 | * @param contextClass The class of the context 40 | * @param argumentClass The class of the argument to events/actions 41 | * @param handler The state machine handler 42 | * @sample io.jumpco.open.kfsm.example.TurnstileFSM.Companion.definition 43 | */ 44 | inline fun asyncStateMachine( 45 | validStates: Set, 46 | validEvents: Set, 47 | contextClass: KClass, 48 | argumentClass: KClass, 49 | handler: AsyncDslStateMachineHandler.() -> Unit 50 | ) = asyncStateMachine(validStates, validEvents, contextClass, argumentClass, Any::class, handler) 51 | 52 | /** 53 | * Defines the start of a state machine DSL declaration with `Any` as the type of arguments and returns types for events/actions 54 | * @param validStates A set of the possible states supported by the top-level state map 55 | * @param validEvents The class of the possible events 56 | * @param contextClass The class of the context 57 | * @sample io.jumpco.open.kfsm.example.TurnstileFSM.Companion.definition 58 | */ 59 | inline fun asyncStateMachine( 60 | validStates: Set, 61 | validEvents: Set, 62 | contextClass: KClass, 63 | handler: AsyncDslStateMachineHandler.() -> Unit 64 | ) = asyncStateMachine(validStates, validEvents, contextClass, Any::class, Any::class, handler) 65 | 66 | inline fun asyncFunctionalStateMachine( 67 | validStates: Set, 68 | validEvents: Set, 69 | contextClass: KClass, 70 | handler: AsyncDslStateMachineHandler.() -> Unit 71 | ) = AsyncStateMachineBuilder(validStates, validEvents).stateMachine(handler) 72 | 73 | /** 74 | * An extension function that evaluates the expression and invokes the provided `block` if true or the `otherwise` block is false. 75 | */ 76 | inline fun T.ifApply(expression: Boolean, block: T.() -> Unit, otherwise: T.() -> Unit): T { 77 | return if (expression) { 78 | this.apply(block) 79 | } else { 80 | this.apply(otherwise) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/commonMain/kotlin/io/jumpco/open/kfsm/stateMachine.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm 20 | 21 | import kotlin.reflect.KClass 22 | 23 | /** 24 | * Defines the start of a state machine DSL declaration 25 | * @author Corneil du Plessis 26 | * @soundtrack Wolfgang Amadeus Mozart 27 | * @param validStates A set of the possible states supported by the top-level state map 28 | * @param validEvents The class of the possible events 29 | * @param contextClass The class of the context 30 | * @param argumentClass The class of the argument to events/actions 31 | * @param returnClass The class of the return type of events/actions 32 | * @param handler Statemachine handler 33 | * @sample io.jumpco.open.kfsm.example.TurnstileFSM.Companion.definition 34 | */ 35 | inline fun stateMachine( 36 | validStates: Set, 37 | validEvents: Set, 38 | contextClass: KClass, 39 | argumentClass: KClass, 40 | returnClass: KClass, 41 | handler: DslStateMachineHandler.() -> Unit 42 | ) = StateMachineBuilder(validStates, validEvents).stateMachine(handler) 43 | 44 | /** 45 | * Defines the start of a state machine DSL declaration with `Any` as the return type 46 | * @param validStates A set of the possible states supported by the top-level state map 47 | * @param eventClass The class of the possible events 48 | * @param contextClass The class of the context 49 | * @param argumentClass The class of the argument to events/actions 50 | * @sample io.jumpco.open.kfsm.example.TurnstileFSM.Companion.definition 51 | */ 52 | inline fun stateMachine( 53 | validStates: Set, 54 | validEvents: Set, 55 | contextClass: KClass, 56 | argumentClass: KClass, 57 | handler: DslStateMachineHandler.() -> Unit 58 | ) = stateMachine( 59 | validStates, 60 | validEvents, 61 | contextClass, 62 | argumentClass, 63 | Any::class, 64 | handler 65 | ) 66 | 67 | /** 68 | * Defines the start of a state machine DSL declaration with `Any` as the type of arguments and returns types for events/actions 69 | * @param validStates A set of the possible states supported by the top-level state map 70 | * @param validEvents The class of the possible events 71 | * @param contextClass The class of the context 72 | * @param handler The DSL handler 73 | * @sample io.jumpco.open.kfsm.example.TurnstileFSM.definition 74 | */ 75 | inline fun stateMachine( 76 | validStates: Set, 77 | validEvents: Set, 78 | contextClass: KClass, 79 | handler: DslStateMachineHandler.() -> Unit 80 | ) = stateMachine( 81 | validStates, 82 | validEvents, 83 | contextClass, 84 | Any::class, 85 | Any::class, 86 | handler 87 | ) 88 | 89 | inline fun functionalStateMachine( 90 | validStates: Set, 91 | validEvents: Set, 92 | contextClass: KClass, 93 | handler: DslStateMachineHandler.() -> Unit 94 | ) = StateMachineBuilder(validStates, validEvents).stateMachine(handler) 95 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/CarFSMTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.functionalStateMachine 22 | import kotlin.test.Test 23 | 24 | /* 25 | * Copyright (c) 2020-2021. Open JumpCO 26 | * Licensed under the Apache License, Version 2.0 (the "License"); 27 | * you may not use this file except in compliance with the License. 28 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 29 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 30 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 31 | * See the License for the specific language governing permissions and limitations under the License. 32 | */ 33 | 34 | /** 35 | * This example was inspired by http://stephen-walsh.com/?p=264 36 | */ 37 | sealed class CarState(val identifier: String) { 38 | override fun toString(): String { 39 | return identifier 40 | } 41 | } 42 | 43 | object Driving : CarState("Driving") 44 | object Parked : CarState("Parked") 45 | object Crashed : CarState("Crashed") 46 | object BeingRepaired : CarState("BeingRepaired") 47 | 48 | sealed class CarEntity(val carType: String, val carModel: String, val yearOfManufacture: String) { 49 | override fun toString(): String { 50 | return "CarEntity(carType='$carType', carModel='$carModel', yearOfManufacture='$yearOfManufacture')" 51 | } 52 | } 53 | 54 | object Uninitialised : CarEntity("", "", "") 55 | object VWGolf : CarEntity("VW", "GOLF", "2016") 56 | 57 | data class Car(val carEntity: CarEntity, val state: CarState? = null) 58 | 59 | class CarFSM(val car: Car) { 60 | val fsm = definition.create(car) 61 | fun sendEvent(event: CarState): Car { 62 | val result = fsm.sendEvent(event, car) ?: car 63 | return result.copy(state = fsm.currentState) 64 | } 65 | 66 | companion object { 67 | private val definition = functionalStateMachine( 68 | setOf(Driving, Parked, Crashed, BeingRepaired), 69 | setOf(Driving, Parked, Crashed, BeingRepaired), 70 | Car::class 71 | ) { 72 | initialState { state ?: Parked } 73 | onStateChange { oldState, newState -> 74 | println("onStateChange:$oldState -> $newState") 75 | } 76 | default { 77 | onEntry { fromState, toState, _ -> 78 | println("onEntry:$fromState -> $toState") 79 | } 80 | action { state, event, _ -> 81 | println("We cannot go from $state, to state $event (data = $this)") 82 | this 83 | } 84 | } 85 | whenState(Parked) { 86 | onEvent(Driving to Driving) { 87 | this 88 | } 89 | onEvent(BeingRepaired to BeingRepaired) { 90 | this 91 | } 92 | } 93 | whenState(Driving) { 94 | onEvent(Parked to Parked) { 95 | this 96 | } 97 | onEvent(Crashed to Crashed) { 98 | println("We just crashed!!!") 99 | this 100 | } 101 | } 102 | whenState(Crashed) { 103 | onEvent(BeingRepaired to BeingRepaired) { 104 | this 105 | } 106 | } 107 | }.build() 108 | } 109 | } 110 | 111 | operator fun Car.plus(event: CarState): Car { 112 | val fsm = CarFSM(this) 113 | return fsm.sendEvent(event) 114 | } 115 | 116 | class CarFSMTest { 117 | @Test 118 | fun testCarFSM() { 119 | var vwGolf = Car(VWGolf) 120 | vwGolf += Driving 121 | vwGolf += Parked 122 | vwGolf += Crashed 123 | vwGolf += Driving 124 | vwGolf += Crashed 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/ImmutableLockFSM.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.functionalStateMachine 22 | import kotlin.test.Test 23 | import kotlin.test.assertEquals 24 | 25 | /** 26 | * @suppress 27 | */ 28 | // tag::context[] 29 | data class ImmutableLock(val locked: Int = 1) { 30 | 31 | fun lock(): ImmutableLock { 32 | require(locked == 0) 33 | println("Lock") 34 | return copy(locked = locked + 1) 35 | } 36 | 37 | fun doubleLock(): ImmutableLock { 38 | require(locked == 1) 39 | println("DoubleLock") 40 | return copy(locked = locked + 1) 41 | } 42 | 43 | fun unlock(): ImmutableLock { 44 | require(locked == 1) 45 | println("Unlock") 46 | return copy(locked = locked - 1) 47 | } 48 | 49 | fun doubleUnlock(): ImmutableLock { 50 | require(locked == 2) 51 | println("DoubleUnlock") 52 | return copy(locked = locked - 1) 53 | } 54 | 55 | override fun toString(): String { 56 | return "Lock(locked=$locked)" 57 | } 58 | } 59 | // end::context[] 60 | 61 | // tag::definition[] 62 | class ImmutableLockFSM { 63 | 64 | companion object { 65 | fun handleEvent(context: ImmutableLock, event: LockEvents): ImmutableLock { 66 | val fsm = definition.create(context) 67 | return fsm.sendEvent(event, context) ?: error("Expected context not null") 68 | } 69 | 70 | private val definition = functionalStateMachine( 71 | LockStates.entries.toSet(), 72 | LockEvents.entries.toSet(), 73 | ImmutableLock::class 74 | ) { 75 | defaultInitialState = LockStates.LOCKED 76 | initialState { 77 | when (locked) { 78 | 0 -> LockStates.UNLOCKED 79 | 1 -> LockStates.LOCKED 80 | 2 -> LockStates.DOUBLE_LOCKED 81 | else -> error("Invalid state locked=$locked") 82 | } 83 | } 84 | invariant("invalid locked value") { locked in 0..2 } 85 | onStateChange { oldState, newState -> 86 | println("onStateChange:$oldState -> $newState") 87 | } 88 | default { 89 | action { state, event, _ -> 90 | println("Default action for state($state) -> on($event) for $this") 91 | this 92 | } 93 | onEntry { startState, targetState, _ -> 94 | println("entering:$startState -> $targetState for $this") 95 | } 96 | onExit { startState, targetState, _ -> 97 | println("exiting:$startState -> $targetState for $this") 98 | } 99 | } 100 | whenState(LockStates.LOCKED) { 101 | onEvent(LockEvents.LOCK to LockStates.DOUBLE_LOCKED) { 102 | doubleLock() 103 | } 104 | onEvent(LockEvents.UNLOCK to LockStates.UNLOCKED) { 105 | unlock() 106 | } 107 | } 108 | whenState(LockStates.DOUBLE_LOCKED) { 109 | onEvent(LockEvents.UNLOCK to LockStates.LOCKED) { 110 | doubleUnlock() 111 | } 112 | } 113 | whenState(LockStates.UNLOCKED) { 114 | onEvent(LockEvents.LOCK to LockStates.LOCKED) { 115 | lock() 116 | } 117 | } 118 | }.build() 119 | } 120 | } 121 | 122 | operator fun ImmutableLock.plus(event: LockEvents): ImmutableLock { 123 | return ImmutableLockFSM.handleEvent(this, event) 124 | } 125 | // end::definition[] 126 | 127 | class TestFunctionalLock { 128 | @Test 129 | fun testState() { 130 | // tag::test[] 131 | val lock = ImmutableLock(0) 132 | val locked = lock + LockEvents.LOCK 133 | assertEquals(locked.locked, 1) 134 | val doubleLocked = locked + LockEvents.LOCK 135 | assertEquals(doubleLocked.locked, 2) 136 | // end::test[] 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/KeyboardBuffer.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.stateMachine 22 | import kotlin.test.Test 23 | import kotlin.test.assertEquals 24 | 25 | /* 26 | * Copyright (c) 2020-2021. Open JumpCO 27 | * Licensed under the Apache License, Version 2.0 (the "License"); 28 | * you may not use this file except in compliance with the License. 29 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 30 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 31 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 32 | * See the License for the specific language governing permissions and limitations under the License. 33 | */ 34 | 35 | enum class KeyboardBufferStates { 36 | DEFAULT, 37 | CAPS_LOCKED 38 | } 39 | 40 | enum class KeyboardEvent { 41 | CAPS_LOCK, 42 | ANY_KEY 43 | } 44 | 45 | class KeyboardBuffer { 46 | private val buffer: MutableList = mutableListOf() 47 | fun add(ch: Char) { 48 | buffer.add(ch) 49 | } 50 | 51 | fun addUpperCase(ch: Char) { 52 | buffer.add(ch.uppercaseChar()) 53 | } 54 | 55 | fun read(): Char = buffer.removeAt(0) 56 | } 57 | 58 | class KeyboardBufferFSM(context: KeyboardBuffer) { 59 | companion object { 60 | val definition = stateMachine( 61 | KeyboardBufferStates.entries.toSet(), 62 | KeyboardEvent.entries.toSet(), 63 | KeyboardBuffer::class, 64 | Char::class 65 | ) { 66 | initialState { KeyboardBufferStates.DEFAULT } 67 | onStateChange { oldState, newState -> 68 | println("onStateChange:$oldState -> $newState") 69 | } 70 | whenState(KeyboardBufferStates.DEFAULT) { 71 | onEvent(KeyboardEvent.ANY_KEY) { input -> 72 | requireNotNull(input) { "input is required" } 73 | add(input) 74 | } 75 | onEvent(KeyboardEvent.CAPS_LOCK to KeyboardBufferStates.CAPS_LOCKED) {} 76 | } 77 | whenState(KeyboardBufferStates.CAPS_LOCKED) { 78 | onEvent(KeyboardEvent.ANY_KEY) { input -> 79 | requireNotNull(input) { "input is required" } 80 | addUpperCase(input) 81 | } 82 | onEvent(KeyboardEvent.CAPS_LOCK to KeyboardBufferStates.DEFAULT) {} 83 | } 84 | }.build() 85 | } 86 | 87 | private val fsm = definition.create(context) 88 | 89 | fun capsLock() = fsm.sendEvent(KeyboardEvent.CAPS_LOCK) 90 | fun anyKey(ch: Char) = fsm.sendEvent(KeyboardEvent.ANY_KEY, ch) 91 | } 92 | 93 | class KeyboardBufferTest { 94 | @Test 95 | fun testFSM() { 96 | val buffer = KeyboardBuffer() 97 | val fsm = KeyboardBufferFSM(buffer) 98 | fsm.anyKey('A') 99 | assertEquals('A', buffer.read()) 100 | fsm.anyKey('a') 101 | assertEquals('a', buffer.read()) 102 | fsm.capsLock() 103 | fsm.anyKey('B') 104 | assertEquals('B', buffer.read()) 105 | fsm.anyKey('b') 106 | assertEquals('B', buffer.read()) 107 | fsm.capsLock() 108 | fsm.anyKey('a') 109 | assertEquals('a', buffer.read()) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/LockFsmTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020-2024. Open JumpCO 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 6 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, 7 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 8 | * See the License for the specific language governing permissions and limitations under the License. 9 | */ 10 | package io.jumpco.open.kfsm.example 11 | 12 | import io.jumpco.open.kfsm.AnyStateMachineBuilder 13 | import io.jumpco.open.kfsm.AnyStateMachineInstance 14 | import io.jumpco.open.kfsm.example.LockEvents.LOCK 15 | import io.jumpco.open.kfsm.example.LockEvents.UNLOCK 16 | import io.jumpco.open.kfsm.example.LockStates.DOUBLE_LOCKED 17 | import io.jumpco.open.kfsm.example.LockStates.LOCKED 18 | import io.jumpco.open.kfsm.example.LockStates.UNLOCKED 19 | import io.jumpco.open.kfsm.stateMachine 20 | import kotlin.test.Test 21 | import kotlin.test.assertEquals 22 | import kotlin.test.assertTrue 23 | import kotlin.test.fail 24 | 25 | /** 26 | * @suppress 27 | */ 28 | class LockFsmTests { 29 | 30 | private fun verifyLockFSM(fsm: AnyStateMachineInstance, lock: Lock) { 31 | // then 32 | assertTrue { fsm.currentState == LOCKED } 33 | assertTrue { lock.locked == 1 } 34 | // when 35 | fsm.sendEvent(UNLOCK) 36 | // then 37 | assertTrue { fsm.currentState == UNLOCKED } 38 | assertTrue { lock.locked == 0 } 39 | try { 40 | // when 41 | fsm.sendEvent(UNLOCK) 42 | fail("Expected an exception") 43 | } catch (x: Throwable) { 44 | println("Expected:$x") 45 | // then 46 | assertEquals("Already unlocked", x.message) 47 | } 48 | // when 49 | fsm.sendEvent(LOCK) 50 | // then 51 | assertTrue { fsm.currentState == LOCKED } 52 | assertTrue { lock.locked == 1 } 53 | // when 54 | fsm.sendEvent(LOCK) 55 | // then 56 | assertTrue { fsm.currentState == DOUBLE_LOCKED } 57 | assertTrue { lock.locked == 2 } 58 | try { 59 | // when 60 | fsm.sendEvent(LOCK) 61 | fail("Expected an exception") 62 | } catch (x: Throwable) { 63 | println("Expected:$x") 64 | // then 65 | assertEquals("Already double locked", x.message) 66 | } 67 | assertTrue { lock.locked == 2 } 68 | } 69 | 70 | @Test 71 | fun testPlainCreationOfFsm() { 72 | // given 73 | val builder = AnyStateMachineBuilder( 74 | LockStates.entries.toSet(), 75 | LockEvents.entries.toSet() 76 | ) 77 | builder.initialState { 78 | when (locked) { 79 | 0 -> UNLOCKED 80 | 1 -> LOCKED 81 | 2 -> DOUBLE_LOCKED 82 | else -> error("Invalid state locked=$locked") 83 | } 84 | } 85 | builder.transition(LOCKED, UNLOCK, UNLOCKED) { 86 | unlock() 87 | } 88 | builder.transition(LOCKED, LOCK, DOUBLE_LOCKED) { 89 | doubleLock() 90 | } 91 | builder.transition(DOUBLE_LOCKED, UNLOCK, LOCKED) { 92 | doubleUnlock() 93 | } 94 | builder.transition(DOUBLE_LOCKED, LOCK) { 95 | error("Already double locked") 96 | } 97 | builder.transition(UNLOCKED, LOCK, LOCKED) { 98 | lock() 99 | } 100 | builder.transition(UNLOCKED, UNLOCK) { 101 | error("Already unlocked") 102 | } 103 | val definition = builder.complete() 104 | // when 105 | val lock = Lock() 106 | val fsm = definition.create(lock) 107 | // then 108 | verifyLockFSM(fsm, lock) 109 | } 110 | 111 | @Test 112 | fun testDslCreationOfFsm() { 113 | // given 114 | val definition = stateMachine( 115 | LockStates.entries.toSet(), 116 | LockEvents.entries.toSet(), 117 | Lock::class 118 | ) { 119 | initialState { 120 | when (locked) { 121 | 0 -> UNLOCKED 122 | 1 -> LOCKED 123 | 2 -> DOUBLE_LOCKED 124 | else -> error("Invalid state locked=$locked") 125 | } 126 | } 127 | 128 | whenState(LOCKED) { 129 | onEvent(LOCK to DOUBLE_LOCKED) { 130 | doubleLock() 131 | } 132 | onEvent(UNLOCK to UNLOCKED) { 133 | unlock() 134 | } 135 | } 136 | whenState(DOUBLE_LOCKED) { 137 | onEvent(UNLOCK to LOCKED) { 138 | doubleUnlock() 139 | } 140 | onEvent(LOCK) { 141 | error("Already double locked") 142 | } 143 | } 144 | whenState(UNLOCKED) { 145 | onEvent(LOCK to LOCKED) { 146 | lock() 147 | } 148 | onEvent(UNLOCK) { 149 | error("Already unlocked") 150 | } 151 | } 152 | }.build() 153 | // when 154 | val lock = Lock() 155 | val fsm = definition.create(lock) 156 | // then 157 | verifyLockFSM(fsm, lock) 158 | } 159 | 160 | @Test 161 | fun simpleLockTest() { 162 | val lock = Lock(0) 163 | val fsm = LockFSM(lock) 164 | println("--lock1") 165 | fsm.lock() 166 | println("--lock2") 167 | fsm.lock() 168 | println("--lock3") 169 | fsm.lock() 170 | println("--unlock1") 171 | fsm.unlock() 172 | println("--unlock2") 173 | fsm.unlock() 174 | println("--unlock3") 175 | fsm.unlock() 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/LockTypes.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.stateMachine 22 | 23 | /** 24 | * @suppress 25 | */ 26 | class Lock(initial: Int = 1) { 27 | var locked: Int = initial 28 | private set 29 | 30 | fun lock() { 31 | require(locked == 0) 32 | println("Lock") 33 | locked += 1 34 | } 35 | 36 | fun doubleLock() { 37 | require(locked == 1) 38 | println("DoubleLock") 39 | locked += 1 40 | } 41 | 42 | fun unlock() { 43 | require(locked == 1) 44 | println("Unlock") 45 | locked -= 1 46 | } 47 | 48 | fun doubleUnlock() { 49 | require(locked == 2) 50 | println("DoubleUnlock") 51 | locked -= 1 52 | } 53 | 54 | override fun toString(): String { 55 | return "Lock(locked=$locked)" 56 | } 57 | } 58 | 59 | enum class LockStates { 60 | LOCKED, 61 | DOUBLE_LOCKED, 62 | UNLOCKED 63 | } 64 | 65 | enum class LockEvents { 66 | LOCK, 67 | UNLOCK 68 | } 69 | 70 | class LockFSM(context: Lock) { 71 | private val fsm = definition.create(context) 72 | 73 | fun allowedEvents() = fsm.allowed() 74 | fun unlock() = fsm.sendEvent(LockEvents.UNLOCK) 75 | fun lock() = fsm.sendEvent(LockEvents.LOCK) 76 | 77 | companion object { 78 | private val definition = stateMachine( 79 | LockStates.entries.toSet(), 80 | LockEvents.entries.toSet(), 81 | Lock::class 82 | ) { 83 | defaultInitialState = LockStates.LOCKED 84 | initialState { 85 | when (locked) { 86 | 0 -> LockStates.UNLOCKED 87 | 1 -> LockStates.LOCKED 88 | 2 -> LockStates.DOUBLE_LOCKED 89 | else -> error("Invalid state locked=$locked") 90 | } 91 | } 92 | invariant("invalid locked value") { locked in 0..2 } 93 | onStateChange { oldState, newState -> 94 | println("onStateChange:$oldState -> $newState") 95 | } 96 | default { 97 | action { state, event, _ -> 98 | println("Default action for state($state) -> on($event) for $this") 99 | } 100 | onEntry { startState, targetState, _ -> 101 | println("entering:$startState -> $targetState for $this") 102 | } 103 | onExit { startState, targetState, _ -> 104 | println("exiting:$startState -> $targetState for $this") 105 | } 106 | } 107 | whenState(LockStates.LOCKED) { 108 | onEvent(LockEvents.LOCK to LockStates.DOUBLE_LOCKED) { 109 | doubleLock() 110 | } 111 | onEvent(LockEvents.UNLOCK to LockStates.UNLOCKED) { 112 | unlock() 113 | } 114 | } 115 | whenState(LockStates.DOUBLE_LOCKED) { 116 | onEvent(LockEvents.UNLOCK to LockStates.LOCKED) { 117 | doubleUnlock() 118 | } 119 | } 120 | whenState(LockStates.UNLOCKED) { 121 | onEvent(LockEvents.LOCK to LockStates.LOCKED) { 122 | lock() 123 | } 124 | } 125 | }.build() 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/PayingTurnstileFsmTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. Open JumpCO 3 | * 4 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 5 | * 6 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 7 | * You should have received a copy of the GNU General Public License along with this program. If not, see . 8 | */ 9 | package io.jumpco.open.kfsm.example 10 | 11 | import kotlin.test.Test 12 | import kotlin.test.assertTrue 13 | 14 | /** 15 | * @suppress 16 | */ 17 | class PayingTurnstileFsmTests { 18 | @Test 19 | fun fsmComponentTest() { 20 | // tag::test[] 21 | val fsm = PayingTurnstileFSM(50) 22 | assertTrue(fsm.turnstile.locked) 23 | println("External:${fsm.externalState()}") 24 | println("--coin1") 25 | fsm.coin(10) 26 | assertTrue(fsm.turnstile.locked) 27 | assertTrue(fsm.turnstile.coins == 10) 28 | println("--coin2") 29 | println("External:${fsm.externalState()}") 30 | val externalState = fsm.externalState() 31 | PayingTurnstileFSM(50, externalState).apply { 32 | coin(60) 33 | assertTrue(turnstile.coins == 0) 34 | assertTrue(!turnstile.locked) 35 | println("External:${externalState()}") 36 | println("--pass1") 37 | pass() 38 | assertTrue(turnstile.locked) 39 | println("--pass2") 40 | pass() 41 | println("--pass3") 42 | pass() 43 | println("--coin3") 44 | coin(40) 45 | assertTrue(turnstile.coins == 40) 46 | println("--coin4") 47 | coin(10) 48 | assertTrue(turnstile.coins == 0) 49 | assertTrue(!turnstile.locked) 50 | } 51 | // end::test[] 52 | } 53 | 54 | @Test 55 | fun fsmComponentTestExternalState() { 56 | val fsm = PayingTurnstileFSM(50) 57 | assertTrue(fsm.turnstile.locked) 58 | println("External:${fsm.externalState()}") 59 | println("--coin1") 60 | fsm.coin(10) 61 | assertTrue(fsm.turnstile.locked) 62 | assertTrue(fsm.turnstile.coins == 10) 63 | println("--coin2") 64 | val es1 = fsm.externalState() 65 | println("External:$es1") 66 | val es2 = PayingTurnstileFSM(50, es1).apply { 67 | this.coin(60) 68 | assertTrue(turnstile.coins == 0) 69 | assertTrue(!turnstile.locked) 70 | println("External:${this.externalState()}") 71 | println("--pass1") 72 | }.externalState() 73 | val es3 = PayingTurnstileFSM(50, es2).apply { 74 | this.pass() 75 | assertTrue(turnstile.locked) 76 | println("--pass2") 77 | }.externalState() 78 | val es4 = PayingTurnstileFSM(50, es3).apply { 79 | this.pass() 80 | println("--pass3") 81 | }.externalState() 82 | val es5 = PayingTurnstileFSM(50, es4).apply { 83 | this.pass() 84 | println("--coin3") 85 | }.externalState() 86 | val es6 = PayingTurnstileFSM(50, es5).apply { 87 | this.coin(40) 88 | assertTrue(turnstile.coins == 40) 89 | println("--coin4") 90 | }.externalState() 91 | PayingTurnstileFSM(50, es6).apply { 92 | this.coin(10) 93 | assertTrue(turnstile.coins == 0) 94 | assertTrue(!turnstile.locked) 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/SecureTurnstile.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.stateMachine 22 | 23 | // tag::states-events[] 24 | enum class SecureTurnstileEvents { 25 | CARD, 26 | PASS 27 | } 28 | // end::states-events[] 29 | 30 | enum class SecureTurnstileStates { 31 | LOCKED, 32 | UNLOCKED 33 | } 34 | 35 | // tag::context[] 36 | class SecureTurnstile { 37 | var locked: Boolean = true 38 | private set 39 | var overrideActive: Boolean = false 40 | private set 41 | 42 | fun activateOverride() { 43 | overrideActive = true 44 | println("override activated") 45 | } 46 | 47 | fun cancelOverride() { 48 | overrideActive = false 49 | println("override canceled") 50 | } 51 | 52 | fun lock() { 53 | println("lock") 54 | locked = true 55 | overrideActive = false 56 | } 57 | 58 | fun unlock() { 59 | println("unlock") 60 | locked = false 61 | overrideActive = false 62 | } 63 | 64 | fun buzzer() { 65 | println("BUZZER") 66 | } 67 | 68 | fun invalidCard(cardId: Int) { 69 | println("Invalid card $cardId") 70 | } 71 | 72 | fun isOverrideCard(cardId: Int): Boolean { 73 | return cardId == 42 74 | } 75 | 76 | fun isValidCard(cardId: Int): Boolean { 77 | return cardId % 2 == 1 78 | } 79 | } 80 | // end::context[] 81 | 82 | // tag::fsm[] 83 | class SecureTurnstileFSM(secureTurnstile: SecureTurnstile) { 84 | companion object { 85 | val definition = stateMachine( 86 | SecureTurnstileStates.entries.toSet(), 87 | SecureTurnstileEvents.entries.toSet(), 88 | SecureTurnstile::class, 89 | Int::class 90 | ) { 91 | defaultInitialState = SecureTurnstileStates.LOCKED 92 | initialState { if (locked) SecureTurnstileStates.LOCKED else SecureTurnstileStates.UNLOCKED } 93 | default { 94 | action { _, _, _ -> 95 | buzzer() 96 | } 97 | } 98 | onStateChange { oldState, newState -> 99 | println("onStateChange:$oldState -> $newState") 100 | } 101 | whenState(SecureTurnstileStates.LOCKED) { 102 | onEvent( 103 | SecureTurnstileEvents.CARD, 104 | guard = { cardId -> 105 | requireNotNull(cardId) 106 | isOverrideCard(cardId) && overrideActive 107 | } 108 | ) { 109 | cancelOverride() 110 | } 111 | onEvent( 112 | SecureTurnstileEvents.CARD, 113 | guard = { cardId -> 114 | requireNotNull(cardId) 115 | isOverrideCard(cardId) 116 | } 117 | ) { 118 | activateOverride() 119 | } 120 | onEvent( 121 | SecureTurnstileEvents.CARD to SecureTurnstileStates.UNLOCKED, 122 | guard = { cardId -> 123 | requireNotNull(cardId) 124 | overrideActive || isValidCard(cardId) 125 | } 126 | ) { 127 | unlock() 128 | } 129 | onEvent( 130 | SecureTurnstileEvents.CARD, 131 | guard = { cardId -> 132 | requireNotNull(cardId) { "cardId is required" } 133 | !isValidCard(cardId) 134 | } 135 | ) { cardId -> 136 | requireNotNull(cardId) 137 | invalidCard(cardId) 138 | } 139 | } 140 | whenState(SecureTurnstileStates.UNLOCKED) { 141 | onEvent( 142 | SecureTurnstileEvents.CARD to SecureTurnstileStates.LOCKED, 143 | guard = { cardId -> 144 | requireNotNull(cardId) 145 | isOverrideCard(cardId) 146 | } 147 | ) { 148 | lock() 149 | } 150 | onEvent(SecureTurnstileEvents.PASS to SecureTurnstileStates.LOCKED) { 151 | lock() 152 | } 153 | } 154 | }.build() 155 | } 156 | 157 | private val fsm = definition.create(secureTurnstile) 158 | fun card(cardId: Int) = fsm.sendEvent(SecureTurnstileEvents.CARD, cardId) 159 | fun pass() = fsm.sendEvent(SecureTurnstileEvents.PASS) 160 | fun allowEvent(): Set = fsm.allowed().map { it.name.lowercase() }.toSet() 161 | } 162 | // end::fsm[] 163 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/SecureTurnstileTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. Open JumpCO 3 | * 4 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 5 | * 6 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 7 | * You should have received a copy of the GNU General Public License along with this program. If not, see . 8 | */ 9 | 10 | package io.jumpco.open.kfsm.example 11 | 12 | import kotlin.test.Test 13 | import kotlin.test.assertTrue 14 | 15 | class SecureTurnstileTests { 16 | @Test 17 | fun testNormalOperation() { 18 | // given 19 | val turnstile = SecureTurnstile() 20 | val fsm = SecureTurnstileFSM(turnstile) 21 | // when 22 | assertTrue { fsm.allowEvent() == setOf("card") } 23 | assertTrue { turnstile.locked } 24 | fsm.card(1) 25 | assertTrue { !turnstile.locked } 26 | assertTrue { fsm.allowEvent() == setOf("pass", "card") } 27 | fsm.pass() 28 | assertTrue { turnstile.locked } 29 | } 30 | 31 | @Test 32 | fun testInvalidCard() { 33 | // given 34 | val turnstile = SecureTurnstile() 35 | val fsm = SecureTurnstileFSM(turnstile) 36 | // when 37 | assertTrue { fsm.allowEvent() == setOf("card") } 38 | assertTrue { turnstile.locked } 39 | fsm.card(2) 40 | assertTrue { fsm.allowEvent() == setOf("card") } 41 | assertTrue { turnstile.locked } 42 | fsm.pass() 43 | assertTrue { turnstile.locked } 44 | } 45 | 46 | @Test 47 | fun testInvalidCardOverride() { 48 | // given 49 | val turnstile = SecureTurnstile() 50 | val fsm = SecureTurnstileFSM(turnstile) 51 | // when 52 | assertTrue { turnstile.locked } 53 | fsm.card(2) 54 | assertTrue { turnstile.locked } 55 | fsm.card(42) // override card 56 | fsm.card(2) 57 | assertTrue { !turnstile.locked } 58 | fsm.pass() 59 | assertTrue { turnstile.locked } 60 | } 61 | 62 | @Test 63 | fun testCancelOverrideToLock() { 64 | // given 65 | val turnstile = SecureTurnstile() 66 | val fsm = SecureTurnstileFSM(turnstile) 67 | // when 68 | assertTrue { turnstile.locked } 69 | fsm.card(2) 70 | assertTrue { turnstile.locked } 71 | fsm.card(42) // override card 72 | fsm.card(2) 73 | assertTrue { !turnstile.locked } 74 | assertTrue { fsm.allowEvent() == setOf("card", "pass") } 75 | fsm.card(42) // override card 76 | assertTrue { turnstile.locked } 77 | } 78 | 79 | @Test 80 | fun testCancelOverride() { 81 | // given 82 | val turnstile = SecureTurnstile() 83 | val fsm = SecureTurnstileFSM(turnstile) 84 | // when 85 | assertTrue { turnstile.locked } 86 | fsm.card(2) 87 | assertTrue { turnstile.locked } 88 | fsm.card(42) // override card 89 | assertTrue { turnstile.overrideActive } 90 | fsm.card(42) // override card 91 | assertTrue { !turnstile.overrideActive } 92 | fsm.card(2) 93 | assertTrue { turnstile.locked } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/commonTest/kotlin/io/jumpco/open/kfsm/example/TimeoutSecureTurnstile.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2024 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.async.asyncStateMachine 22 | import kotlinx.coroutines.CoroutineScope 23 | 24 | // tag::states-events[] 25 | enum class TimeoutSecureTurnstileEvents { 26 | CARD, 27 | PASS 28 | } 29 | // end::states-events[] 30 | 31 | enum class TimeoutSecureTurnstileStates { 32 | LOCKED, 33 | UNLOCKED 34 | } 35 | 36 | // tag::context[] 37 | class TimerSecureTurnstile { 38 | var locked: Boolean = true 39 | private set 40 | var overrideActive: Boolean = false 41 | private set 42 | val timeout = 500L 43 | fun activateOverride() { 44 | overrideActive = true 45 | println("override activated") 46 | } 47 | 48 | fun cancelOverride() { 49 | overrideActive = false 50 | println("override canceled") 51 | } 52 | 53 | fun lock() { 54 | println("lock") 55 | locked = true 56 | overrideActive = false 57 | } 58 | 59 | fun unlock() { 60 | println("unlock") 61 | locked = false 62 | overrideActive = false 63 | } 64 | 65 | fun buzzer() { 66 | println("BUZZER") 67 | } 68 | 69 | fun invalidCard(cardId: Int) { 70 | println("Invalid card $cardId") 71 | } 72 | 73 | fun isOverrideCard(cardId: Int): Boolean { 74 | return cardId == 42 75 | } 76 | 77 | fun isValidCard(cardId: Int): Boolean { 78 | return cardId % 2 == 1 79 | } 80 | } 81 | // end::context[] 82 | 83 | // tag::fsm[] 84 | class TimerSecureTurnstileFSM(secureTurnstile: TimerSecureTurnstile, coroutineScope: CoroutineScope) { 85 | companion object { 86 | val definition = asyncStateMachine( 87 | TimeoutSecureTurnstileStates.entries.toSet(), 88 | TimeoutSecureTurnstileEvents.entries.toSet(), 89 | TimerSecureTurnstile::class, 90 | Int::class 91 | ) { 92 | defaultInitialState = TimeoutSecureTurnstileStates.LOCKED 93 | initialState { if (locked) TimeoutSecureTurnstileStates.LOCKED else TimeoutSecureTurnstileStates.UNLOCKED } 94 | default { 95 | action { _, _, _ -> 96 | buzzer() 97 | } 98 | } 99 | onStateChange { oldState, newState -> 100 | println("onStateChange:$oldState -> $newState") 101 | } 102 | whenState(TimeoutSecureTurnstileStates.LOCKED) { 103 | onEvent( 104 | TimeoutSecureTurnstileEvents.CARD, 105 | guard = { cardId -> 106 | requireNotNull(cardId) 107 | isOverrideCard(cardId) && overrideActive 108 | } 109 | ) { 110 | cancelOverride() 111 | } 112 | onEvent( 113 | TimeoutSecureTurnstileEvents.CARD, 114 | guard = { cardId -> 115 | requireNotNull(cardId) 116 | isOverrideCard(cardId) 117 | } 118 | ) { 119 | activateOverride() 120 | } 121 | onEvent( 122 | TimeoutSecureTurnstileEvents.CARD to TimeoutSecureTurnstileStates.UNLOCKED, 123 | guard = { cardId -> 124 | requireNotNull(cardId) 125 | overrideActive || isValidCard(cardId) 126 | } 127 | ) { 128 | unlock() 129 | } 130 | onEvent( 131 | TimeoutSecureTurnstileEvents.CARD, 132 | guard = { cardId -> 133 | requireNotNull(cardId) { "cardId is required" } 134 | !isValidCard(cardId) 135 | } 136 | ) { cardId -> 137 | requireNotNull(cardId) 138 | invalidCard(cardId) 139 | } 140 | } 141 | whenState(TimeoutSecureTurnstileStates.UNLOCKED) { 142 | timeout(TimeoutSecureTurnstileStates.LOCKED, { timeout }) { 143 | println("Timeout. Locking") 144 | lock() 145 | } 146 | onEvent( 147 | TimeoutSecureTurnstileEvents.CARD to TimeoutSecureTurnstileStates.LOCKED, 148 | guard = { cardId -> 149 | requireNotNull(cardId) 150 | isOverrideCard(cardId) 151 | } 152 | ) { 153 | lock() 154 | } 155 | onEvent(TimeoutSecureTurnstileEvents.PASS to TimeoutSecureTurnstileStates.LOCKED) { 156 | lock() 157 | } 158 | } 159 | }.build() 160 | } 161 | 162 | private val fsm = definition.create(secureTurnstile, coroutineScope) 163 | 164 | suspend fun card(cardId: Int) = fsm.sendEvent(TimeoutSecureTurnstileEvents.CARD, cardId) 165 | suspend fun pass() = fsm.sendEvent(TimeoutSecureTurnstileEvents.PASS) 166 | fun allowEvent(): Set = fsm.allowed().map { it.name.lowercase() }.toSet() 167 | } 168 | // end::fsm[] 169 | -------------------------------------------------------------------------------- /src/docs/asciidoc/docinfo-footer.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/docs/asciidoc/docinfo.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = KFSM - Kotlin Finite-state machine 2 | :toc: 3 | :toclevels: 3 4 | 5 | == Introduction 6 | 7 | Having used various incarnations of link:http://smc.sourceforge.net/[SMC] over the years we decided to try it again and discovered it didn't have specific support for Kotlin. 8 | Instead of creating a generator for Kotlin we decided to attempt a Kotlin DSL along with a simple implementation. 9 | 10 | To learn more about Finite State Machines visit: 11 | 12 | * link:https://en.wikipedia.org/wiki/Finite-state_machine[Wikipedia - Finite-state machine] 13 | * link:https://brilliant.org/wiki/finite-state-machines/[Brilliant - Finite State Machines] 14 | * link:http://smc.sourceforge.net/slides/SMC_Tutorial.pdf[SMC Tutorial] 15 | 16 | The <> provides examples of how to compose a DSL and FSM. 17 | 18 | == Links 19 | 20 | === link:javadoc/kfsm/index.html[API Docs] 21 | 22 | === link:https://github.com/open-jumpco/kfsm[Source] 23 | 24 | == Features 25 | 26 | This Finite state machine implementation has the following features: 27 | 28 | * Event driven state machine. 29 | * External and internal transitions 30 | * State entry and exit actions. 31 | * Default state actions. 32 | * Default entry and exit actions. 33 | * Determine allowed events for current or given state. 34 | * Multiple state maps with push / pop transitions 35 | * Automatic transitions 36 | * Externalisation of state. 37 | * Simple Visualization. 38 | * Detailed Visualization. 39 | * Visualization Gradle Plugin 40 | * Coroutines 41 | * Timeout Transitions 42 | * StateChange notification action 43 | 44 | == Tutorial 45 | 46 | include::kfsm.adoc[] 47 | 48 | include::documentation.adoc[] 49 | -------------------------------------------------------------------------------- /src/docs/asciidoc/lock-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/lock-fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/lock_fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/lock_fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/packet-reader-fsm-guard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/packet-reader-fsm-guard.png -------------------------------------------------------------------------------- /src/docs/asciidoc/packet-reader-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/packet-reader-fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/packet_reader_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/packet_reader_detail.png -------------------------------------------------------------------------------- /src/docs/asciidoc/paying-turnstile-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/paying-turnstile-fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/paying_turnstile_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/paying_turnstile_detail.png -------------------------------------------------------------------------------- /src/docs/asciidoc/prism.css: -------------------------------------------------------------------------------- 1 | /* PrismJS 1.17.1 2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+java+javastacktrace+javadoclike+json+jsonp+json5+kotlin+js-templates+js-extras+javadoc+yaml */ 3 | /** 4 | * prism.js default theme for JavaScript, CSS and HTML 5 | * Based on dabblet (http://dabblet.com) 6 | * @author Lea Verou 7 | */ 8 | 9 | code[class*="language-"], 10 | pre[class*="language-"] { 11 | color: black; 12 | background: none; 13 | text-shadow: 0 1px white; 14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace; 15 | font-size: 1em; 16 | text-align: left; 17 | white-space: pre; 18 | word-spacing: normal; 19 | word-break: normal; 20 | word-wrap: normal; 21 | line-height: 1.5; 22 | 23 | -moz-tab-size: 4; 24 | -o-tab-size: 4; 25 | tab-size: 4; 26 | 27 | -webkit-hyphens: none; 28 | -moz-hyphens: none; 29 | -ms-hyphens: none; 30 | hyphens: none; 31 | } 32 | 33 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 34 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 35 | text-shadow: none; 36 | background: #b3d4fc; 37 | } 38 | 39 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 40 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 41 | text-shadow: none; 42 | background: #b3d4fc; 43 | } 44 | 45 | @media print { 46 | code[class*="language-"], 47 | pre[class*="language-"] { 48 | text-shadow: none; 49 | } 50 | } 51 | 52 | /* Code blocks */ 53 | pre[class*="language-"] { 54 | padding: 1em; 55 | margin: .5em 0; 56 | overflow: auto; 57 | } 58 | 59 | :not(pre) > code[class*="language-"], 60 | pre[class*="language-"] { 61 | background: #f5f2f0; 62 | } 63 | 64 | /* Inline code */ 65 | :not(pre) > code[class*="language-"] { 66 | padding: .1em; 67 | border-radius: .3em; 68 | white-space: normal; 69 | } 70 | 71 | .token.comment, 72 | .token.prolog, 73 | .token.doctype, 74 | .token.cdata { 75 | color: slategray; 76 | } 77 | 78 | .token.punctuation { 79 | color: #999; 80 | } 81 | 82 | .namespace { 83 | opacity: .7; 84 | } 85 | 86 | .token.property, 87 | .token.tag, 88 | .token.boolean, 89 | .token.number, 90 | .token.constant, 91 | .token.symbol, 92 | .token.deleted { 93 | color: #905; 94 | } 95 | 96 | .token.selector, 97 | .token.attr-name, 98 | .token.string, 99 | .token.char, 100 | .token.builtin, 101 | .token.inserted { 102 | color: #690; 103 | } 104 | 105 | .token.operator, 106 | .token.entity, 107 | .token.url, 108 | .language-css .token.string, 109 | .style .token.string { 110 | color: #9a6e3a; 111 | background: hsla(0, 0%, 100%, .5); 112 | } 113 | 114 | .token.atrule, 115 | .token.attr-value, 116 | .token.keyword { 117 | color: #07a; 118 | } 119 | 120 | .token.function, 121 | .token.class-name { 122 | color: #DD4A68; 123 | } 124 | 125 | .token.regex, 126 | .token.important, 127 | .token.variable { 128 | color: #e90; 129 | } 130 | 131 | .token.important, 132 | .token.bold { 133 | font-weight: bold; 134 | } 135 | 136 | .token.italic { 137 | font-style: italic; 138 | } 139 | 140 | .token.entity { 141 | cursor: help; 142 | } 143 | 144 | -------------------------------------------------------------------------------- /src/docs/asciidoc/sample-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/sample-fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/secure-turnstile-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/secure-turnstile-fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/secure_turnstile_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/secure_turnstile_detail.png -------------------------------------------------------------------------------- /src/docs/asciidoc/statemachine-classes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/statemachine-classes.png -------------------------------------------------------------------------------- /src/docs/asciidoc/statemachine-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/statemachine-model.png -------------------------------------------------------------------------------- /src/docs/asciidoc/statemachine-packaged.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/statemachine-packaged.png -------------------------------------------------------------------------------- /src/docs/asciidoc/statemachine-sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/statemachine-sequence.png -------------------------------------------------------------------------------- /src/docs/asciidoc/timeout_turnstile_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/timeout_turnstile_detail.png -------------------------------------------------------------------------------- /src/docs/asciidoc/turnstile-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/turnstile-fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/turnstile-scxml.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/docs/asciidoc/turnstile-sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/turnstile-sequence.png -------------------------------------------------------------------------------- /src/docs/asciidoc/turnstile_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/turnstile_detail.png -------------------------------------------------------------------------------- /src/docs/asciidoc/turnstile_fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/turnstile_fsm.png -------------------------------------------------------------------------------- /src/docs/asciidoc/turnstile_scxml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/turnstile_scxml.png -------------------------------------------------------------------------------- /src/docs/asciidoc/useless-fsm.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/src/docs/asciidoc/useless-fsm.png -------------------------------------------------------------------------------- /src/docs/plantuml/kfsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam classFontSize 9 4 | skinparam classFontName Monospaced 5 | skinparam state { 6 | BackgroundColor #1b74bc 7 | } 8 | 9 | [*] --right-> Convoluted : Code 10 | Convoluted --right-> Organized : KFSM 11 | 12 | @enduml 13 | -------------------------------------------------------------------------------- /src/docs/plantuml/lock-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam StateFontName Helvetica 5 | skinparam defaultFontName Monospaced 6 | skinparam defaultFontStyle Bold 7 | skinparam state { 8 | FontColor Black 9 | FontStyle Bold 10 | } 11 | 12 | 13 | [*] --> LOCKED 14 | LOCKED --> UNLOCKED : UNLOCK 15 | UNLOCKED ---> LOCKED : LOCK 16 | DOUBLE_LOCKED ---> LOCKED : UNLOCK 17 | LOCKED --> DOUBLE_LOCKED : LOCK 18 | 19 | @enduml 20 | -------------------------------------------------------------------------------- /src/docs/plantuml/packet-reader-fsm-guard.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam classFontSize 9 5 | skinparam classFontName Monospaced 6 | 7 | [*] --> RCVPCKT : CTRL { byte == SOH } 8 | RCVPCKT --> RCVDATA : CTRL { byte == STX } / addField 9 | RCVDATA --> RCVDATA : BYTE / addByte(byte) 10 | RCVDATA --> RCVPCKT : CTRL { guard == ETX } / endField 11 | RCVDATA --> RCVESC : ESC 12 | RCVDATA --> [*] : CTRL / sendNACK 13 | RCVESC --> RCVDATA: ESC \n addByte(byte) 14 | RCVESC --> RCVDATA: CTRL \n addByte(byte) 15 | RCVPCKT --> RCVCHK : BYTE / addChecksum(byte) 16 | RCVCHK --> RCVCHK : BYTE / addChecksum(byte) 17 | RCVCHK --> CHKSUM : CTRL {guard == EOT } / checksum 18 | RCVCHK --> RCVCHKESC : ESC 19 | RCVCHKESC --> RCVCHK : ESC / addChecksum(byte) 20 | RCVCHKESC --> RCVCHK : CTRL / addChecksum(byte) 21 | RCVCHK --> [*] : CTRL / sendNACK 22 | CHKSUM --> [*] : <> { checksumValid }\n sendACK 23 | CHKSUM --> [*] : <> { !checksumValid }\n sendNACK 24 | @enduml 25 | -------------------------------------------------------------------------------- /src/docs/plantuml/packet-reader-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | skinparam monochrome true 3 | skinparam classFontSize 9 4 | skinparam classFontName Monospaced 5 | 6 | [*] --> RCVPCKT : SOH 7 | RCVPCKT --> RCVDATA : STX / addField 8 | RCVDATA --> RCVDATA : BYTE / addByte(byte) 9 | RCVDATA --> RCVPCKT : ETX / endField 10 | RCVDATA --> RCVESC : ESC 11 | RCVDATA --> [*] : ETX|EOT|SOH|STX\n sendNACK 12 | RCVESC --> RCVDATA: ESC|ETX|EOT|SOH|STX\n addByte(byte) 13 | RCVPCKT --> RCVCHK : BYTE / addChecksum(byte) 14 | RCVCHK --> RCVCHK : BYTE / addChecksum(byte) 15 | RCVCHK --> CHKSUM : EOT / checksum 16 | RCVCHK --> RCVCHKESC : ESC 17 | RCVCHKESC --> RCVCHK : ESC|ETX|EOT|SOH|STX\n addChecksum(byte) 18 | RCVCHK --> [*] : ETX|EOT|SOH|STX\n sendNACK 19 | CHKSUM --> [*] : <> { checksumValid }\n sendACK 20 | CHKSUM --> [*] : <> { !checksumValid }\n sendNACK 21 | @enduml 22 | -------------------------------------------------------------------------------- /src/docs/plantuml/paying-turnstile-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam classFontSize 9 5 | skinparam classFontName Monospaced 6 | 7 | [*] -right-> LOCKED 8 | 9 | state LOCKED { 10 | LOCKED --> LOCKED : <> PASS\n{ alarm() } 11 | LOCKED --> COINS : COIN(value)\n{ coin(value); } 12 | } 13 | 14 | state UNLOCKED { 15 | UNLOCKED --> LOCKED : PASS\n{ lock() } 16 | UNLOCKED --up--> COINS : COIN(value)\n{ coin(value); } 17 | } 18 | 19 | state coins { 20 | state COINS { 21 | COINS --> COINS : COIN(value)\n{ coin(value); } 22 | COINS --down--> UNLOCKED : <>\nguard:{ value + coins == requiredCoins }\n{ unlock() } 23 | COINS --down--> UNLOCKED : <>\nguard:{ value + coins > requiredCoins }\n{ returnCoin(); unlock(); } 24 | } 25 | } 26 | 27 | @enduml 28 | -------------------------------------------------------------------------------- /src/docs/plantuml/sample-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam StateFontName Helvetica 5 | skinparam defaultFontName Monospaced 6 | skinparam defaultFontStyle Bold 7 | skinparam state { 8 | FontColor Black 9 | FontStyle Bold 10 | } 11 | 12 | 13 | [*] --> STATE1 14 | STATE1 ---> STATE2 : EVENT1\n{ action1() } 15 | STATE2 ---> STATE1 : EVENT2\n{ action2() } 16 | STATE1 ---> STATE1 : EVENT2\n{ action3() } 17 | STATE2 ---> STATE2 : EVENT1\n{ action4() } 18 | 19 | @enduml 20 | -------------------------------------------------------------------------------- /src/docs/plantuml/secure-turnstile-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam StateFontName Helvetica 5 | skinparam defaultFontName Monospaced 6 | skinparam defaultFontStyle Bold 7 | skinparam state { 8 | FontColor Black 9 | FontStyle Bold 10 | } 11 | 12 | [*] -right-> LOCKED 13 | 14 | LOCKED ----> UNLOCKED : CARD(id) [{ isOverrideCard(id) }] -> {\l activateOverride()\l} 15 | LOCKED ----> UNLOCKED : CARD(id) [{ isValid(id) || overrideActive }] -> {\l unlock()\l}] 16 | LOCKED ----> UNLOCKED : CARD(id) [{ !isValid(id) }] -> {\l error("Invalid card")\l} 17 | 18 | UNLOCKED ----> LOCKED : PASS\l{\l lock()\l} 19 | UNLOCKED ----> LOCKED : CARD(id)\l [{ isOverrideCard(id) }] -> {\l lock()\l} 20 | 21 | @enduml 22 | -------------------------------------------------------------------------------- /src/docs/plantuml/statemachine-classes.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | 5 | class Context { 6 | } 7 | enum States { 8 | } 9 | enum Events { 10 | } 11 | 12 | class StateMachineBuilder { 13 | transition(startState: S, event: E[, targetState: S][, guard: C.() -> Boolean], action: C.(Any[]) -> Unit) 14 | automatic(startState: S, targetState: S[, guard: C.() -> Boolean], action: C.(Any[]) -> Unit) 15 | automaticPop(startState: S[, targetState: S][, targetMap: String][, guard: C.() -> Boolean], action: C.(Any[]) -> Unit) 16 | automaticPush(startState: S, targetState: S, targetMap: String[, guard: C.() -> Boolean], action: C.(Any[]) -> Unit) 17 | create(context: Context, initialState: States?) : StateMachineInstance 18 | } 19 | 20 | class StateMachineDefinition { 21 | allowed(currentState: S, includeDefaults: Boolean): Set 22 | eventAllowed(event: E, currentState: S, includeDefault: Boolean): Boolean 23 | create(context: C, initialState: S?) : StateMachineInstance 24 | } 25 | 26 | class StateMachineInstance { 27 | StateMachineInstance(context: Context, fsm: StateMachine, initialState: S?) 28 | Context: context 29 | var currentState: S 30 | sendEvent(event: Events, Any[]) 31 | } 32 | 33 | StateMachineBuilder ..left..> States : <> 34 | StateMachineBuilder ..right..> Events : <> 35 | StateMachineBuilder ..up.> Context : <> 36 | 37 | StateMachineDefinition .left.> S : <> 38 | StateMachineDefinition .right.> E : <> 39 | StateMachineDefinition .up.> C : <> 40 | 41 | StateMachineInstance *---> StateMachineDefinition : definition 42 | StateMachineInstance *--left--> Context : context 43 | 44 | @enduml 45 | -------------------------------------------------------------------------------- /src/docs/plantuml/statemachine-model.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | 5 | class C { 6 | } 7 | enum S { 8 | } 9 | enum E { 10 | } 11 | 12 | class StateMachineBuilder { 13 | initial(action: C.(Any[]) -> S) 14 | initialMap(action: C.(Any[]) -> List>) 15 | transition(startState: S, event: E, targetState: S[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 16 | automatic(startState: S, targetState: S[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 17 | automaticPop(startState: S[, targetState: S][, targetMap: String][, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 18 | automaticPush(startState: S, targetState: S, targetMap: String[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 19 | pushTransition(startState: S, event: E, targetMap: String, targetState: S[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 20 | popTransition(startState: S, event: E[, targetMap: String][, targetState: S][, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 21 | entry(currentState: S, action: C.(Any[]) -> Unit) 22 | exit(currentState: S, action: C.(Any[]) -> Unit) 23 | defaultTransition(startState: S, event: E[, targetState: S], action: C.(Any[]) -> Unit) 24 | defaultAction(action: C.( S, E, Any[]) -> Unit) 25 | defaultEntry(action: C.( S, S, Any[]) -> Unit) 26 | defaultExit(action: C.( S, S, Any[]) -> Unit) 27 | default(currentState: S, action: C.( S, E, Any[]) -> Unit) 28 | stateMachine() 29 | } 30 | 31 | class StateMachineDefinition { 32 | allowed(currentState: S, includeDefaults: Boolean): Set 33 | eventAllowed(event: E, currentState: S, includeDefault: Boolean): Boolean 34 | create(context: C, initialState: S?) : StateMachineInstance 35 | } 36 | 37 | class StateMachineInstance { 38 | StateMachineInstance(context: Context, fsm: StateMachine, initialState: S?) 39 | currentState: S 40 | sendEvent(event: E, Any[]) 41 | allowed(includeDefaults: Boolean): Set 42 | eventAllowed(event: E, includeDefault: Boolean): Boolean 43 | } 44 | 45 | StateMachineBuilder .left.> S : <> 46 | StateMachineBuilder .right.> E : <> 47 | StateMachineBuilder .up.> C : <> 48 | 49 | StateMachineDefinition .left.> S : <> 50 | StateMachineDefinition .right.> E : <> 51 | StateMachineDefinition .up.> C : <> 52 | 53 | StateMachineInstance *---> StateMachineDefinition : definition 54 | StateMachineInstance *-left-> C : context 55 | 56 | @enduml 57 | -------------------------------------------------------------------------------- /src/docs/plantuml/statemachine-packaged.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | 5 | class Turnstile { 6 | lock() 7 | unlock() 8 | alarm() 9 | returnCoin() 10 | } 11 | 12 | class TurnstileFSM { 13 | TurnstileFSM(context: Turnstile) 14 | coin() 15 | pass() 16 | } 17 | 18 | class StateMachineInstance { 19 | Turnstile: context 20 | currentState: TurnstileStates 21 | sendEvent(event: TurnstileEvents) 22 | } 23 | 24 | class StateMachineDefinition { 25 | transition(startState: S, event: E, targetState: S, action: C.(Any[]) -> Unit) 26 | automatic(startState: S, targetState: S[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 27 | automaticPop(startState: S[, targetState: S][, targetMap: String][, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 28 | automaticPush(startState: S, targetState: S, targetMap: String[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 29 | pushTransition(startState: S, event: E, targetMap: String, targetState: S[, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 30 | popTransition(startState: S, event: E[, targetMap: String][, targetState: S][, guard: C.()->Boolean], action: C.(Any[]) -> Unit) 31 | create(context: Turnstile, initialState: TurnstileStates?) : StateMachineInstance 32 | stateMachine(): StateMachine 33 | } 34 | 35 | 36 | TurnstileFSM *--> StateMachineInstance: stateMachine 37 | StateMachineInstance *--> StateMachineDefinition : definition 38 | StateMachineInstance *-up-> Turnstile : context 39 | TurnstileFSM ...left...> Turnstile 40 | TurnstileFSM ....> StateMachine : <> 41 | 42 | @enduml 43 | -------------------------------------------------------------------------------- /src/docs/plantuml/statemachine-sequence.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | 5 | actor Consumer 6 | boundary StateMachineInstance 7 | control Context 8 | 9 | Consumer ----> StateMachineInstance : sendEvent(event:E) 10 | 11 | activate StateMachineInstance 12 | note right of StateMachineInstance 13 | StateMachineInstance determines the TransitionRules 14 | or default exit / entry / action(s) needed 15 | end note 16 | activate StateMachineInstance 17 | StateMachineInstance ----> StateMachineInstance : exit {} 18 | StateMachineInstance ----> Context : action {} 19 | activate Context 20 | note right of Context 21 | Context performs the action(s) 22 | end note 23 | Context ----> StateMachineInstance 24 | deactivate Context 25 | StateMachineInstance ----> StateMachineInstance : entry {} 26 | StateMachineInstance ----> StateMachineInstance : executeAutomatic 27 | deactivate StateMachineInstance 28 | return 29 | 30 | @enduml 31 | -------------------------------------------------------------------------------- /src/docs/plantuml/turnstile-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam StateFontName Helvetica 5 | skinparam defaultFontName Monospaced 6 | skinparam defaultFontStyle Bold 7 | skinparam state { 8 | FontColor Black 9 | FontStyle Bold 10 | } 11 | 12 | [*] --> LOCKED 13 | LOCKED ---> UNLOCKED : COIN\n{ unlock() } 14 | UNLOCKED ---> LOCKED : PASS\n{ lock() } 15 | LOCKED ---> LOCKED : PASS\n{ alarm() } 16 | UNLOCKED ---> UNLOCKED : COIN\n{ returnCoin() } 17 | 18 | @enduml 19 | -------------------------------------------------------------------------------- /src/docs/plantuml/turnstile-sequence.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | 5 | actor Consumer 6 | boundary TurnStileFSM 7 | control Turnstile 8 | 9 | Consumer -------> TurnStileFSM : coin() 10 | TurnStileFSM ------> Turnstile : unlock() 11 | Consumer -------> TurnStileFSM : coin() 12 | TurnStileFSM ------> Turnstile : returnCoin() 13 | Consumer -------> TurnStileFSM : pass() 14 | TurnStileFSM ------> Turnstile : lock() 15 | Consumer -------> TurnStileFSM : pass() 16 | TurnStileFSM ------> Turnstile : alarm() 17 | 18 | @enduml 19 | -------------------------------------------------------------------------------- /src/docs/plantuml/useless-fsm.plantuml: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | skinparam monochrome true 4 | skinparam StateFontName Helvetica 5 | skinparam defaultFontName Monospaced 6 | skinparam defaultFontStyle Bold 7 | skinparam state { 8 | FontColor Black 9 | FontStyle Bold 10 | } 11 | 12 | [*] --left-> CLOSED 13 | 14 | CLOSED ---> OPENED : SWITCH\n{ openLid() } 15 | OPENED ---> CLOSED : <>\n{ flipSwitch() } 16 | 17 | @enduml 18 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/io/jumpco/open/kfsm/example/TimeoutSecureTurnstileTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. Open JumpCO 3 | * 4 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 5 | * 6 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 7 | * You should have received a copy of the GNU General Public License along with this program. If not, see . 8 | */ 9 | 10 | package io.jumpco.open.kfsm.example 11 | 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.runBlocking 16 | import kotlin.test.Test 17 | import kotlin.test.assertTrue 18 | 19 | class TimeoutSecureTurnstileTests { 20 | private val coroutineScope = CoroutineScope(Dispatchers.Default) 21 | 22 | @Test 23 | fun testNormalOperation() { 24 | // given 25 | val turnstile = TimerSecureTurnstile() 26 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 27 | // when 28 | assertTrue { fsm.allowEvent() == setOf("card") } 29 | assertTrue { turnstile.locked } 30 | runBlocking { 31 | println("Card 1") 32 | fsm.card(1) 33 | println("Asserting") 34 | assertTrue { !turnstile.locked } 35 | assertTrue { fsm.allowEvent() == setOf("pass", "card") } 36 | println("Pass") 37 | fsm.pass() 38 | println("Asserting") 39 | assertTrue { turnstile.locked } 40 | } 41 | } 42 | 43 | @Test 44 | fun testInvalidCard() { 45 | // given 46 | val turnstile = TimerSecureTurnstile() 47 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 48 | // when 49 | assertTrue { fsm.allowEvent() == setOf("card") } 50 | assertTrue { turnstile.locked } 51 | runBlocking { 52 | fsm.card(2) 53 | assertTrue { fsm.allowEvent() == setOf("card") } 54 | assertTrue { turnstile.locked } 55 | fsm.pass() 56 | assertTrue { turnstile.locked } 57 | } 58 | } 59 | 60 | @Test 61 | fun testInvalidCardOverride() { 62 | // given 63 | val turnstile = TimerSecureTurnstile() 64 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 65 | // when 66 | assertTrue { turnstile.locked } 67 | runBlocking { 68 | fsm.card(2) 69 | assertTrue { turnstile.locked } 70 | fsm.card(42) // override card 71 | fsm.card(2) 72 | assertTrue { !turnstile.locked } 73 | fsm.pass() 74 | assertTrue { turnstile.locked } 75 | } 76 | } 77 | 78 | @Test 79 | fun testCancelOverrideToLock() { 80 | // given 81 | val turnstile = TimerSecureTurnstile() 82 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 83 | // when 84 | assertTrue { turnstile.locked } 85 | runBlocking { 86 | fsm.card(2) 87 | assertTrue { turnstile.locked } 88 | fsm.card(42) // override card 89 | fsm.card(2) 90 | assertTrue { !turnstile.locked } 91 | assertTrue { fsm.allowEvent() == setOf("card", "pass") } 92 | fsm.card(42) // override card 93 | assertTrue { turnstile.locked } 94 | } 95 | } 96 | 97 | @Test 98 | fun testCancelOverride() { 99 | // given 100 | val turnstile = TimerSecureTurnstile() 101 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 102 | // when 103 | assertTrue { turnstile.locked } 104 | runBlocking { 105 | fsm.card(2) 106 | assertTrue { turnstile.locked } 107 | fsm.card(42) // override card 108 | assertTrue { turnstile.overrideActive } 109 | fsm.card(42) // override card 110 | assertTrue { !turnstile.overrideActive } 111 | fsm.card(2) 112 | assertTrue { turnstile.locked } 113 | } 114 | } 115 | 116 | // tag::test[] 117 | @Test 118 | fun testTimeout() { 119 | val turnstile = TimerSecureTurnstile() 120 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 121 | assertTrue { turnstile.locked } 122 | println("Card 1") 123 | runBlocking { 124 | fsm.card(1) 125 | println("Assertion") 126 | assertTrue { !turnstile.locked } 127 | println("Delay:100") 128 | delay(100L) 129 | assertTrue { !turnstile.locked } 130 | println("Delay:1000") 131 | delay(1000L) 132 | println("Assertion") 133 | assertTrue { turnstile.locked } 134 | } 135 | } 136 | // end::test[] 137 | } 138 | -------------------------------------------------------------------------------- /src/jvmTest/kotlin/io/jumpco/open/kfsm/example/VisualizeTurnstileTest.kt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2019-2021 Open JumpCO 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, 7 | and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial 10 | portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO 13 | THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 15 | CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 16 | DEALINGS IN THE SOFTWARE. 17 | */ 18 | 19 | package io.jumpco.open.kfsm.example 20 | 21 | import io.jumpco.open.kfsm.viz.Parser 22 | import io.jumpco.open.kfsm.viz.Visualization 23 | import java.io.File 24 | import kotlin.test.BeforeTest 25 | import kotlin.test.Test 26 | 27 | class VisualizeTurnstileTest { 28 | @BeforeTest 29 | fun setup() { 30 | val generated = File("generated") 31 | if (generated.exists() && !generated.isDirectory) { 32 | error("Expected generated to be a directory") 33 | } else if (!generated.exists()) { 34 | generated.mkdirs() 35 | } 36 | } 37 | 38 | @Test 39 | fun produceVisualizationTurnstileFSM() { 40 | println("== TurnStile") 41 | val visualisation = Parser.parseStateMachine( 42 | "TurnstileFSM", 43 | File("src/commonTest/kotlin/io/jumpco/open/kfsm/example/TurnstileTypes.kt") 44 | ) 45 | println(visualisation) 46 | File("generated", "turnstile.plantuml").writeText(Visualization.plantUml(visualisation)) 47 | } 48 | 49 | @Test 50 | fun produceVisualizationPayingTurnstile() { 51 | println("== PayingTurnstile") 52 | val visualization = Parser.parseStateMachine( 53 | "PayingTurnstileFSM", 54 | File("src/commonTest/kotlin/io/jumpco/open/kfsm/example/PayingTurnstileTypes.kt") 55 | ) 56 | println(visualization) 57 | File("generated", "paying-turnstile.plantuml").writeText(Visualization.plantUml(visualization)) 58 | } 59 | 60 | @Test 61 | fun produceVisualizationSecureTurnstile() { 62 | println("== SecureTurnstile") 63 | val visualization = Parser.parseStateMachine( 64 | "SecureTurnstileFSM", 65 | File("src/commonTest/kotlin/io/jumpco/open/kfsm/example/SecureTurnstile.kt") 66 | ) 67 | println(visualization) 68 | File("generated", "secure-turnstile.plantuml").writeText(Visualization.plantUml(visualization)) 69 | } 70 | 71 | @Test 72 | fun produceVisualizationPacketReader() { 73 | println("== PacketReader") 74 | val visualization = Parser.parseStateMachine( 75 | "PacketReaderFSM", 76 | File("src/commonTest/kotlin/io/jumpco/open/kfsm/example/PacketReaderTests.kt") 77 | ) 78 | println(visualization) 79 | File("generated", "packet-reader.plantuml").writeText(Visualization.plantUml(visualization)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/nativeTest/kotlin/io/jumpco/open/kfsm/example/TimeoutSecureTurnstileTests.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024. Open JumpCO 3 | * 4 | * This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. 5 | * 6 | * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. 7 | * You should have received a copy of the GNU General Public License along with this program. If not, see . 8 | */ 9 | 10 | package io.jumpco.open.kfsm.example 11 | 12 | import kotlinx.coroutines.CoroutineScope 13 | import kotlinx.coroutines.Dispatchers 14 | import kotlinx.coroutines.delay 15 | import kotlinx.coroutines.runBlocking 16 | import kotlin.test.Test 17 | import kotlin.test.assertTrue 18 | 19 | class TimeoutSecureTurnstileTests { 20 | private val coroutineScope = CoroutineScope(Dispatchers.Default) 21 | 22 | @Test 23 | fun testNormalOperation() { 24 | // given 25 | val turnstile = TimerSecureTurnstile() 26 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 27 | // when 28 | assertTrue { fsm.allowEvent() == setOf("card") } 29 | assertTrue { turnstile.locked } 30 | runBlocking { 31 | println("Card 1") 32 | fsm.card(1) 33 | println("Asserting") 34 | assertTrue { !turnstile.locked } 35 | assertTrue { fsm.allowEvent() == setOf("pass", "card") } 36 | println("Pass") 37 | fsm.pass() 38 | println("Asserting") 39 | assertTrue { turnstile.locked } 40 | } 41 | } 42 | 43 | @Test 44 | fun testInvalidCard() { 45 | // given 46 | val turnstile = TimerSecureTurnstile() 47 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 48 | // when 49 | assertTrue { fsm.allowEvent() == setOf("card") } 50 | assertTrue { turnstile.locked } 51 | runBlocking { 52 | fsm.card(2) 53 | assertTrue { fsm.allowEvent() == setOf("card") } 54 | assertTrue { turnstile.locked } 55 | fsm.pass() 56 | assertTrue { turnstile.locked } 57 | } 58 | } 59 | 60 | @Test 61 | fun testInvalidCardOverride() { 62 | // given 63 | val turnstile = TimerSecureTurnstile() 64 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 65 | // when 66 | assertTrue { turnstile.locked } 67 | runBlocking { 68 | fsm.card(2) 69 | assertTrue { turnstile.locked } 70 | fsm.card(42) // override card 71 | fsm.card(2) 72 | assertTrue { !turnstile.locked } 73 | fsm.pass() 74 | assertTrue { turnstile.locked } 75 | } 76 | } 77 | 78 | @Test 79 | fun testCancelOverrideToLock() { 80 | // given 81 | val turnstile = TimerSecureTurnstile() 82 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 83 | // when 84 | assertTrue { turnstile.locked } 85 | runBlocking { 86 | fsm.card(2) 87 | assertTrue { turnstile.locked } 88 | fsm.card(42) // override card 89 | fsm.card(2) 90 | assertTrue { !turnstile.locked } 91 | assertTrue { fsm.allowEvent() == setOf("card", "pass") } 92 | fsm.card(42) // override card 93 | assertTrue { turnstile.locked } 94 | } 95 | } 96 | 97 | @Test 98 | fun testCancelOverride() { 99 | // given 100 | val turnstile = TimerSecureTurnstile() 101 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 102 | // when 103 | assertTrue { turnstile.locked } 104 | runBlocking { 105 | fsm.card(2) 106 | assertTrue { turnstile.locked } 107 | fsm.card(42) // override card 108 | assertTrue { turnstile.overrideActive } 109 | fsm.card(42) // override card 110 | assertTrue { !turnstile.overrideActive } 111 | fsm.card(2) 112 | assertTrue { turnstile.locked } 113 | } 114 | } 115 | 116 | // tag::test[] 117 | @Test 118 | fun testTimeout() { 119 | val turnstile = TimerSecureTurnstile() 120 | val fsm = TimerSecureTurnstileFSM(turnstile, coroutineScope) 121 | assertTrue { turnstile.locked } 122 | 123 | println("Card 1") 124 | runBlocking { 125 | fsm.card(1) 126 | println("Assertion") 127 | assertTrue { !turnstile.locked } 128 | println("Delay:100") 129 | delay(100L) 130 | assertTrue { !turnstile.locked } 131 | println("Delay:1000") 132 | delay(1000L) 133 | println("Assertion") 134 | assertTrue { turnstile.locked } 135 | } 136 | } 137 | // end::test[] 138 | } 139 | -------------------------------------------------------------------------------- /turnstile.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/open-jumpco/kfsm/25c0f744ad21bfcedb51b8a4946b3d724489aec9/turnstile.png --------------------------------------------------------------------------------