├── settings.gradle ├── Docs └── img │ ├── timetravel.gif │ ├── reswift_detail.png │ ├── reswift_concept.png │ └── reswift_concept.graffle ├── .gitignore ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── LICENSE ├── src ├── main │ └── kotlin │ │ ├── State.kt │ │ ├── Reducer.kt │ │ ├── StoreSubscriber.kt │ │ ├── Middleware.kt │ │ ├── Action.kt │ │ ├── DispatchingStoreType.kt │ │ ├── StoreType.kt │ │ ├── Store.kt │ │ └── Subscription.kt └── test │ └── kotlin │ ├── StoreTests.kt │ ├── StoreDispatchTests.kt │ ├── TestFakes.kt │ ├── StoreMiddlewareTests.kt │ ├── StoreSubscriptionTests.kt │ └── StoreSubscriberTests.kt ├── gradlew.bat ├── gradlew └── README.md /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'rekotlin' 2 | 3 | -------------------------------------------------------------------------------- /Docs/img/timetravel.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoThings/ReKotlin/HEAD/Docs/img/timetravel.gif -------------------------------------------------------------------------------- /Docs/img/reswift_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoThings/ReKotlin/HEAD/Docs/img/reswift_detail.png -------------------------------------------------------------------------------- /Docs/img/reswift_concept.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoThings/ReKotlin/HEAD/Docs/img/reswift_concept.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ml 2 | .gradle 3 | /local.properties 4 | /.idea 5 | .DS_Store 6 | /build 7 | /captures 8 | /artifacts 9 | -------------------------------------------------------------------------------- /Docs/img/reswift_concept.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoThings/ReKotlin/HEAD/Docs/img/reswift_concept.graffle -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GeoThings/ReKotlin/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed Aug 09 11:36:34 CST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 GeoThings 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 | -------------------------------------------------------------------------------- /src/main/kotlin/State.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 31/07/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | interface StateType {} 28 | 29 | -------------------------------------------------------------------------------- /src/main/kotlin/Reducer.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 31/07/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | typealias Reducer = (action: Action, state: ReducerStateType?) -> ReducerStateType -------------------------------------------------------------------------------- /src/main/kotlin/StoreSubscriber.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 07/08/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | interface StoreSubscriber { 28 | fun newState(state: StoreSubscriberStateType) 29 | } -------------------------------------------------------------------------------- /src/main/kotlin/Middleware.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 31/07/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | typealias DispatchFunction = (Action) -> Unit 28 | typealias Middleware = (DispatchFunction, () -> State?) -> (DispatchFunction) -> DispatchFunction -------------------------------------------------------------------------------- /src/main/kotlin/Action.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 31/07/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | /** 28 | * All actions that want to be able to be dispatched to a store need to conform to this protocol 29 | * Currently it is just a marker protocol with no requirements. 30 | */ 31 | interface Action {} 32 | 33 | /** 34 | * Initial Action that is dispatched as soon as the store is created. 35 | * Reducers respond to this action by configuring their initial state. 36 | */ 37 | class ReKotlinInit: Action {} -------------------------------------------------------------------------------- /src/main/kotlin/DispatchingStoreType.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 31/07/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | /** 28 | * Defines the interface of a dispatching, stateless Store in ReSwift. `StoreType` is 29 | * the default usage of this interface. Can be used for store variables where you don't 30 | * care about the state, but want to be able to dispatch actions. 31 | */ 32 | interface DispatchingStoreType { 33 | 34 | /** 35 | * Dispatches an action. This is the simplest way to modify the stores state. 36 | * 37 | * Example of dispatching an action: 38 | *
39 |      * 
40 |      * store.dispatch( CounterAction.IncreaseCounter )
41 |      * 
42 |      * 
43 | * 44 | * @param action The action that is being dispatched to the store 45 | * @return By default returns the dispatched action, but middlewares can change the type, e.g. to return promises 46 | */ 47 | fun dispatch(action: Action) 48 | } -------------------------------------------------------------------------------- /src/test/kotlin/StoreTests.kt: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test 2 | import tw.geothings.rekotlin.Action 3 | import tw.geothings.rekotlin.ReKotlinInit 4 | import tw.geothings.rekotlin.StateType 5 | import tw.geothings.rekotlin.Store 6 | 7 | /** 8 | * Created by Taras Vozniuk on 10/08/2017. 9 | * Copyright © 2017 GeoThings. All rights reserved. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining 12 | * a copy of this software and associated documentation files (the 13 | * "Software"), to deal in the Software without restriction, including 14 | * without limitation the rights to use, copy, modify, merge, publish, 15 | * distribute, sublicense, and/or sell copies of the Software, and to 16 | * permit persons to whom the Software is furnished to do so, subject to 17 | * the following conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be 20 | * included in all copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 26 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 28 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | internal class StoreTests { 32 | 33 | /** 34 | * it dispatches an Init action when it doesn't receive an initial state 35 | */ 36 | @Test 37 | fun testInit() { 38 | val reducer = MockReducer() 39 | Store(reducer::handleAction, null) 40 | 41 | assert(reducer.calledWithAction[0] is ReKotlinInit) 42 | } 43 | 44 | // testDeinit() is not relevant in JVM 45 | 46 | } 47 | 48 | internal data class CounterState(var count: Int = 0): StateType 49 | 50 | internal class MockReducer { 51 | 52 | val calledWithAction: MutableList = mutableListOf() 53 | 54 | fun handleAction(action: Action, state: CounterState?): CounterState { 55 | calledWithAction.add(action) 56 | 57 | return state ?: CounterState() 58 | } 59 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/test/kotlin/StoreDispatchTests.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin.junit 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.* 5 | import tw.geothings.rekotlin.* 6 | import java.util.concurrent.CountDownLatch 7 | import java.util.concurrent.TimeUnit 8 | import kotlin.concurrent.thread 9 | 10 | /** 11 | * Created by Taras Vozniuk on 10/08/2017. 12 | * Copyright © 2017 GeoThings. All rights reserved. 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | 34 | internal typealias TestSubscriber = TestStoreSubscriber 35 | internal typealias CallbackSubscriber = CallbackStoreSubscriber 36 | 37 | internal class StoreDispatchTests { 38 | 39 | var reducer = TestReducer() 40 | var store = Store(reducer::handleAction, TestAppState()) 41 | 42 | @BeforeEach 43 | fun setUp() {} 44 | 45 | @AfterEach 46 | fun tearDown() {} 47 | 48 | /** 49 | * it throws an exception when a reducer dispatches an action 50 | */ 51 | @Test 52 | fun testThrowsExceptionWhenReducersDispatch(){ 53 | //TODO: testThrowsExceptionWhenReducersDispatch 54 | } 55 | 56 | /** 57 | * it accepts action creators 58 | */ 59 | @Test 60 | fun testAcceptsActionCreators(){ 61 | store.dispatch(SetValueAction(5)) 62 | 63 | val doubleValueActionCreator: ActionCreator> = { state, store -> 64 | SetValueAction(state.testValue!! * 2) 65 | } 66 | 67 | store.dispatch(doubleValueActionCreator) 68 | assertEquals(10, store.state.testValue) 69 | } 70 | 71 | /** 72 | * it accepts async action creators 73 | */ 74 | @Test 75 | fun testAcceptsAsyncActionCreators() { 76 | 77 | val awaitEntity = CountDownLatch(1) 78 | 79 | val asyncActionCreator: AsyncActionCreator> = { _, _, callback -> 80 | thread { 81 | // Provide the callback with an action creator 82 | callback { _, _ -> 83 | SetValueAction(5) 84 | } 85 | } 86 | } 87 | 88 | val subscriber = CallbackStoreSubscriber { state -> 89 | this.store.state.testValue?.let { 90 | assertEquals(5, it) 91 | awaitEntity.countDown() 92 | } 93 | } 94 | 95 | store.subscribe(subscriber) 96 | store.dispatch(asyncActionCreator) 97 | assertTrue(awaitEntity.await(1, TimeUnit.SECONDS)) 98 | } 99 | 100 | /** 101 | * it calls the callback once state update from async action is complete 102 | */ 103 | @Test 104 | fun testCallsCallbackOnce(){ 105 | 106 | val awaitEntity = CountDownLatch(1) 107 | 108 | val asyncActionCreator: AsyncActionCreator> = { _, _, callback -> 109 | thread { 110 | // Provide the callback with an action creator 111 | callback { _, _ -> 112 | SetValueAction(5) 113 | } 114 | } 115 | } 116 | 117 | store.dispatch(asyncActionCreator) { newState -> 118 | assertEquals(5, this.store.state.testValue) 119 | if (newState.testValue == 5) { 120 | awaitEntity.countDown() 121 | } 122 | } 123 | 124 | assertTrue(awaitEntity.await(1, TimeUnit.SECONDS)) 125 | } 126 | } 127 | 128 | -------------------------------------------------------------------------------- /src/test/kotlin/TestFakes.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin.junit 2 | 3 | import org.junit.jupiter.api.Test 4 | import tw.geothings.rekotlin.* 5 | import javax.swing.plaf.nimbus.State 6 | 7 | /** 8 | * Created by Taras Vozniuk on 10/08/2017. 9 | * Copyright © 2017 GeoThings. All rights reserved. 10 | * 11 | * Permission is hereby granted, free of charge, to any person obtaining 12 | * a copy of this software and associated documentation files (the 13 | * "Software"), to deal in the Software without restriction, including 14 | * without limitation the rights to use, copy, modify, merge, publish, 15 | * distribute, sublicense, and/or sell copies of the Software, and to 16 | * permit persons to whom the Software is furnished to do so, subject to 17 | * the following conditions: 18 | * 19 | * The above copyright notice and this permission notice shall be 20 | * included in all copies or substantial portions of the Software. 21 | * 22 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 23 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 24 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 25 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 26 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 27 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 28 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 29 | */ 30 | 31 | internal data class TestAppState(val testValue: Int? = null): StateType 32 | 33 | internal data class TestStringAppState(val testValue: String = "Initial"): StateType 34 | 35 | internal data class TestCustomSubstate(val value: Int): StateType 36 | 37 | internal data class TestCustomAppState(val substate: TestCustomSubstate): StateType { 38 | constructor(substateValue: Int = 0): this(TestCustomSubstate(substateValue)) 39 | } 40 | 41 | internal data class NoOpAction(val unit: Unit = Unit): Action {} 42 | internal data class SetValueAction(val value: Int?): Action { 43 | companion object { 44 | val type = "SetValueAction" 45 | } 46 | } 47 | 48 | internal data class SetValueStringAction(var value: String): Action { 49 | companion object { 50 | val type = "SetValueStringAction" 51 | } 52 | } 53 | 54 | internal data class SetCustomSubstateAction(val value: Int): Action { 55 | companion object { 56 | val type = "SetCustomSubstateAction" 57 | } 58 | } 59 | 60 | internal class TestReducer { 61 | fun handleAction(action: Action, state: TestAppState?): TestAppState { 62 | @Suppress("NAME_SHADOWING") 63 | var state = state ?: TestAppState() 64 | 65 | when(action){ 66 | is SetValueAction -> { 67 | state = state.copy(testValue = action.value) 68 | } 69 | } 70 | 71 | return state 72 | } 73 | } 74 | 75 | internal class TestValueStringReducer { 76 | fun handleAction(action: Action, state: TestStringAppState?): TestStringAppState { 77 | @Suppress("NAME_SHADOWING") 78 | var state = state ?: TestStringAppState() 79 | 80 | when(action){ 81 | is SetValueStringAction -> { 82 | state = state.copy(testValue = action.value) 83 | } 84 | } 85 | 86 | return state 87 | } 88 | } 89 | 90 | internal class TestCustomAppStateReducer { 91 | fun handleAction(action: Action, state: TestCustomAppState?): TestCustomAppState { 92 | @Suppress("NAME_SHADOWING") 93 | var state = state ?: TestCustomAppState() 94 | 95 | when(action){ 96 | is SetCustomSubstateAction -> { 97 | state = state.copy(substate = state.substate.copy(action.value)) 98 | } 99 | } 100 | 101 | return state 102 | } 103 | 104 | } 105 | 106 | internal class TestStoreSubscriber: StoreSubscriber { 107 | var recievedStates: MutableList = mutableListOf() 108 | 109 | override fun newState(state: T) { 110 | this.recievedStates.add(state) 111 | } 112 | } 113 | 114 | internal class DispatchingSubscriber(var store: Store): StoreSubscriber { 115 | 116 | override fun newState(state: TestAppState) { 117 | // Test if we've already dispatched this action to 118 | // avoid endless recursion 119 | if (state.testValue != 5){ 120 | this.store.dispatch(SetValueAction(5)) 121 | } 122 | } 123 | } 124 | 125 | internal class CallbackStoreSubscriber(val handler: (T) -> Unit): StoreSubscriber { 126 | override fun newState(state: T) { 127 | handler(state) 128 | } 129 | } 130 | 131 | -------------------------------------------------------------------------------- /src/test/kotlin/StoreMiddlewareTests.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin.junit 2 | 3 | import org.junit.jupiter.api.Assertions.* 4 | import org.junit.jupiter.api.Test 5 | import tw.geothings.rekotlin.Middleware 6 | import tw.geothings.rekotlin.StateType 7 | import tw.geothings.rekotlin.Store 8 | 9 | /** 10 | * Created by Taras Vozniuk on 10/08/2017. 11 | * Copyright © 2017 GeoThings. All rights reserved. 12 | * 13 | * Permission is hereby granted, free of charge, to any person obtaining 14 | * a copy of this software and associated documentation files (the 15 | * "Software"), to deal in the Software without restriction, including 16 | * without limitation the rights to use, copy, modify, merge, publish, 17 | * distribute, sublicense, and/or sell copies of the Software, and to 18 | * permit persons to whom the Software is furnished to do so, subject to 19 | * the following conditions: 20 | * 21 | * The above copyright notice and this permission notice shall be 22 | * included in all copies or substantial portions of the Software. 23 | * 24 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 25 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 26 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 27 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 28 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 29 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 30 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 31 | */ 32 | 33 | internal val firstMiddleware: Middleware = { dispatch, getState -> 34 | { next -> 35 | { action -> 36 | (action as? SetValueStringAction)?.let { 37 | it.value += " First Middleware" 38 | next(action) 39 | } ?: next(action) 40 | } 41 | } 42 | } 43 | 44 | internal val secondMiddleware: Middleware = { dispatch, getState -> 45 | { next -> 46 | { action -> 47 | (action as? SetValueStringAction)?.let { 48 | it.value += " Second Middleware" 49 | next(action) 50 | } ?: next(action) 51 | } 52 | } 53 | } 54 | 55 | internal val dispatchingMiddleware: Middleware = { dispatch, getState -> 56 | { next -> 57 | { action -> 58 | (action as? SetValueAction)?.let { 59 | dispatch(SetValueStringAction("${it.value ?: 0}")) 60 | } 61 | 62 | next(action) 63 | } 64 | } 65 | } 66 | 67 | internal val stateAccessingMiddleware: Middleware = { dispatch, getState -> 68 | { next -> 69 | { action -> 70 | 71 | val appState = getState() 72 | val stringAction = action as? SetValueStringAction 73 | 74 | // avoid endless recursion by checking if we've dispatched exactly this action 75 | if (appState?.testValue == "OK" && stringAction?.value != "Not OK"){ 76 | // dispatch a new action 77 | dispatch(SetValueStringAction("Not OK")) 78 | 79 | // and swallow the current one 80 | dispatch(NoOpAction()) 81 | } else { 82 | next(action) 83 | } 84 | } 85 | } 86 | } 87 | 88 | internal class StoreMiddlewareTests { 89 | 90 | /** 91 | * it can decorate dispatch function 92 | */ 93 | @Test 94 | fun testDecorateDispatch(){ 95 | 96 | val reducer = TestValueStringReducer() 97 | val store = Store( 98 | reducer = reducer::handleAction, 99 | state = TestStringAppState(), 100 | middleware = listOf(firstMiddleware, secondMiddleware) 101 | ) 102 | 103 | val subscriber = TestStoreSubscriber() 104 | store.subscribe(subscriber) 105 | 106 | val action = SetValueStringAction("OK") 107 | store.dispatch(action) 108 | 109 | assertEquals("OK First Middleware Second Middleware", store.state.testValue) 110 | } 111 | 112 | /** 113 | * it can dispatch actions 114 | */ 115 | @Test 116 | fun testCanDispatch() { 117 | 118 | val reducer = TestValueStringReducer() 119 | val store = Store( 120 | reducer = reducer::handleAction, 121 | state = TestStringAppState(), 122 | middleware = listOf(firstMiddleware, secondMiddleware, dispatchingMiddleware) 123 | ) 124 | 125 | val subscriber = TestStoreSubscriber() 126 | store.subscribe(subscriber) 127 | 128 | val action = SetValueAction(10) 129 | store.dispatch(action) 130 | 131 | assertEquals("10 First Middleware Second Middleware", store.state.testValue) 132 | } 133 | 134 | /** 135 | * it middleware can access the store's state 136 | */ 137 | @Test 138 | fun testMiddlewareCanAccessState() { 139 | 140 | val reducer = TestValueStringReducer() 141 | var state = TestStringAppState() 142 | state = state.copy(testValue = "OK") 143 | 144 | val store = Store( 145 | reducer = reducer::handleAction, 146 | state = state, 147 | middleware = listOf(stateAccessingMiddleware) 148 | ) 149 | 150 | store.dispatch(SetValueStringAction("Action That Won't Go Through")) 151 | assertEquals("Not OK", store.state.testValue) 152 | } 153 | } 154 | 155 | -------------------------------------------------------------------------------- /src/main/kotlin/StoreType.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 07/08/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | /** 28 | * Defines the interface of Stores in ReSwift. `Store` is the default implementation of this 29 | * interface. Applications have a single store that stores the entire application state. 30 | * Stores receive actions and use reducers combined with these actions, to calculate state changes. 31 | * Upon every state update a store informs all of its subscribers. 32 | */ 33 | interface StoreType: DispatchingStoreType { 34 | 35 | /** 36 | * The current state stored in the store. 37 | */ 38 | val state: State 39 | 40 | /** 41 | * The main dispatch function that is used by all convenience `dispatch` methods. 42 | * This dispatch function can be extended by providing middlewares. 43 | */ 44 | var dispatchFunction: DispatchFunction 45 | 46 | /** 47 | * Subscribes the provided subscriber to this store. 48 | * Subscribers will receive a call to `newState` whenever the 49 | * state in this store changes. 50 | * @param subscriber: Subscriber that will receive store updates 51 | */ 52 | fun > subscribe(subscriber: S) 53 | 54 | /** 55 | * Subscribes the provided subscriber to this store. 56 | * Subscribers will receive a call to `newState` whenever the 57 | * state in this store changes and the subscription decides to forward 58 | * state update. 59 | * 60 | * @param subscriber Subscriber that will receive store updates 61 | * @param transform A closure that receives a simple subscription and can return a 62 | * transformed subscription. Subscriptions can be transformed to only select a subset of the 63 | * state, or to skip certain state updates. 64 | */ 65 | fun > subscribe(subscriber: S, transform: ((Subscription) -> Subscription)?) 66 | 67 | /** 68 | * Unsubscribes the provided subscriber. The subscriber will no longer 69 | * receive state updates from this store. 70 | * 71 | * @param subscriber Subscriber that will be unsubscribed 72 | */ 73 | fun unsubscribe(subscriber: StoreSubscriber) 74 | 75 | /** 76 | * Dispatches an action creator to the store. Action creators are functions that generate 77 | * actions. They are called by the store and receive the current state of the application 78 | * and a reference to the store as their input. 79 | * 80 | * Based on that input the action creator can either return an action or not. Alternatively 81 | * the action creator can also perform an asynchronous operation and dispatch a new action 82 | * at the end of it. 83 | * 84 | * Example of an action creator: 85 | *
 86 |      * 
 87 |      * func deleteNote(noteID: Int) -> ActionCreator {
 88 |      *     return { state, store in
 89 |      *         // only delete note if editing is enabled
 90 |      *         if (state.editingEnabled == true) {
 91 |      *             return NoteDataAction.DeleteNote(noteID)
 92 |      *         } else {
 93 |      *             return nil
 94 |      *         }
 95 |      *      }
 96 |      * }
 97 |      * 
 98 |      * 
99 | * 100 | * This action creator can then be dispatched as following: 101 | *
102 |      * 
103 |      * store.dispatch( noteActionCreatore.deleteNote(3) )
104 |      * 
105 |      * 
106 | * 107 | * @return: By default returns the dispatched action, but middlewares can change the 108 | * return type, e.g. to return promises 109 | */ 110 | fun dispatch(actionCreator: ActionCreator>) 111 | 112 | /** 113 | * Dispatches an async action creator to the store. An async action creator generates an 114 | * action creator asynchronously. 115 | */ 116 | fun dispatch(asyncActionCreator: AsyncActionCreator>) 117 | 118 | /** 119 | * Dispatches an async action creator to the store. An async action creator generates an 120 | * action creator asynchronously. Use this method if you want to wait for the state change 121 | * triggered by the asynchronously generated action creator. 122 | * 123 | * This overloaded version of `dispatch` calls the provided `callback` as soon as the 124 | * asynchronoously dispatched action has caused a new state calculation. 125 | * 126 | * If the ActionCreator does not dispatch an action, the callback block will never 127 | * be called 128 | */ 129 | fun dispatch(asyncActionCreator: AsyncActionCreator>, callback: DispatchCallback?) 130 | } -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 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 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 165 | if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then 166 | cd "$(dirname "$0")" 167 | fi 168 | 169 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 170 | -------------------------------------------------------------------------------- /src/main/kotlin/Store.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 31/07/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | class Store ( 28 | private val reducer: Reducer, 29 | state: State?, 30 | middleware: List> = emptyList(), 31 | automaticallySkipRepeats: Boolean = true): StoreType { 32 | 33 | private var _state: State? = state 34 | set(value) { 35 | val oldValue = field 36 | field = value 37 | 38 | value?.let { 39 | subscriptions.forEach { 40 | it.newValues(oldValue, value) 41 | } 42 | } 43 | } 44 | 45 | override val state: State 46 | get() { return _state!! } 47 | 48 | @Suppress("NAME_SHADOWING") 49 | override var dispatchFunction: DispatchFunction = middleware 50 | .reversed() 51 | .fold({ action: Action -> this._defaultDispatch(action) }, { dispatchFunction, middleware -> 52 | val dispatch = { action: Action -> this.dispatch(action) } 53 | val getState = { this._state } 54 | middleware(dispatch, getState)(dispatchFunction) 55 | }) 56 | 57 | val subscriptions: MutableList> = mutableListOf() 58 | 59 | private var isDispatching = false 60 | 61 | /** 62 | * Indicates if new subscriptions attempt to apply `skipRepeats` by default. 63 | */ 64 | val subscribersAutomaticallySkipsRepeat: Boolean = automaticallySkipRepeats 65 | 66 | init { 67 | this._state?.let { this._state = state } ?: this.dispatch(ReKotlinInit()) 68 | } 69 | 70 | override fun > subscribe(subscriber: S) { 71 | 72 | // if subscribersAutomaticallySkipsRepeat is set 73 | // skipRepeats will be applied with kotlin structural equality 74 | if (subscribersAutomaticallySkipsRepeat){ 75 | this.subscribe(subscriber, { 76 | it.skipRepeats() 77 | }) 78 | } else { 79 | this.subscribe(subscriber, null) 80 | } 81 | } 82 | 83 | override fun > subscribe(subscriber: S, transform: ((Subscription) -> Subscription)?) { 84 | // If the same subscriber is already registered with the store, replace the existing 85 | // subscription with the new one. 86 | val index = this.subscriptions.indexOfFirst { it.subscriber === subscriber } 87 | if (index != -1){ 88 | this.subscriptions.removeAt(index) 89 | } 90 | 91 | // Create a subscription for the new subscriber. 92 | val originalSubscription = Subscription() 93 | // Call the optional transformation closure. This allows callers to modify 94 | // the subscription, e.g. in order to subselect parts of the store's state. 95 | val transformedSubscription = transform?.invoke(originalSubscription) 96 | 97 | val subscriptionBox = SubscriptionBox(originalSubscription, transformedSubscription, subscriber) 98 | 99 | // each subscriber has its own potentially different SelectedState that doesn't have to conform to StateType 100 | @Suppress("UNCHECKED_CAST") 101 | this.subscriptions.add(subscriptionBox as SubscriptionBox) 102 | 103 | this._state?.let { 104 | originalSubscription.newValues(null, it) 105 | } 106 | } 107 | 108 | override fun unsubscribe(subscriber: StoreSubscriber) { 109 | val index = this.subscriptions.indexOfFirst { it.subscriber === subscriber } 110 | if (index != -1){ 111 | this.subscriptions.removeAt(index) 112 | } 113 | } 114 | 115 | fun _defaultDispatch(action: Action){ 116 | if (isDispatching) { 117 | throw Exception( 118 | "ReKotlin:ConcurrentMutationError- Action has been dispatched while" + 119 | " a previous action is action is being processed. A reducer" + 120 | " is dispatching an action, or ReKotlin is used in a concurrent context" + 121 | " (e.g. from multiple threads)." 122 | ) 123 | } 124 | 125 | this.isDispatching = true 126 | val newState = reducer(action, this._state) 127 | this.isDispatching = false 128 | 129 | this._state = newState 130 | } 131 | 132 | override fun dispatch(action: Action){ 133 | this.dispatchFunction(action) 134 | } 135 | 136 | override fun dispatch(actionCreator: ActionCreator>){ 137 | actionCreator(this.state, this)?.let { 138 | this.dispatch(it) 139 | } 140 | } 141 | 142 | override fun dispatch(asyncActionCreator: AsyncActionCreator>){ 143 | this.dispatch(asyncActionCreator, null) 144 | } 145 | 146 | override fun dispatch(asyncActionCreator: AsyncActionCreator>, callback: DispatchCallback?){ 147 | asyncActionCreator(this.state, this) { actionProvider -> 148 | val action = actionProvider(this.state, this) 149 | 150 | action?.let { 151 | this.dispatch(it) 152 | callback?.invoke(this.state) 153 | } 154 | } 155 | } 156 | } 157 | 158 | typealias DispatchCallback = (State) -> Unit 159 | 160 | typealias ActionCreator = (state: State, store: Store) -> Action? 161 | 162 | typealias AsyncActionCreator = (state: State, store: Store, actionCreatorCallback: (ActionCreator) -> Unit) -> Unit -------------------------------------------------------------------------------- /src/test/kotlin/StoreSubscriptionTests.kt: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test 2 | import org.junit.jupiter.api.Assertions.* 3 | import tw.geothings.rekotlin.Store 4 | import tw.geothings.rekotlin.junit.* 5 | 6 | /** 7 | * Created by Taras Vozniuk on 10/08/2017. 8 | * Copyright © 2017 GeoThings. All rights reserved. 9 | * 10 | * Permission is hereby granted, free of charge, to any person obtaining 11 | * a copy of this software and associated documentation files (the 12 | * "Software"), to deal in the Software without restriction, including 13 | * without limitation the rights to use, copy, modify, merge, publish, 14 | * distribute, sublicense, and/or sell copies of the Software, and to 15 | * permit persons to whom the Software is furnished to do so, subject to 16 | * the following conditions: 17 | * 18 | * The above copyright notice and this permission notice shall be 19 | * included in all copies or substantial portions of the Software. 20 | * 21 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 22 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 23 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 24 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 25 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 26 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 27 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 28 | */ 29 | 30 | internal class StoreSubscriptionTests { 31 | 32 | var reducer = TestReducer() 33 | var store = Store(reducer::handleAction, TestAppState()) 34 | 35 | // this is not going to work in JVM. 36 | // WeakReference also can't solve it since gc collects non-deterministically 37 | //TODO: Discuss with ReSwift community for this inconsistency 38 | /* 39 | /** 40 | * It does not strongly capture an observer 41 | */ 42 | @Test 43 | fun testStrongCapture(){ 44 | store = Store(reducer::handleAction, TestAppState()) 45 | var subscriber: TestSubscriber? = TestSubscriber() 46 | 47 | store.subscribe(subscriber!!) 48 | assertEquals(1, store.subscriptions.map { it.subscriber != null }.count()) 49 | 50 | @Suppress("UNUSED_VALUE") 51 | subscriber = null 52 | assertEquals(0, store.subscriptions.map { it.subscriber != null }.count()) 53 | } 54 | */ 55 | 56 | /** 57 | * it removes subscribers before notifying state changes 58 | */ 59 | @Test 60 | fun testRemoveSubscribers(){ 61 | store = Store(reducer::handleAction, TestAppState()) 62 | val subscriber1 = TestSubscriber() 63 | val subscriber2 = TestSubscriber() 64 | 65 | store.subscribe(subscriber1) 66 | store.subscribe(subscriber2) 67 | store.dispatch(SetValueAction(3)) 68 | assertEquals(2, store.subscriptions.count()) 69 | assertEquals(3, subscriber1.recievedStates.lastOrNull()?.testValue) 70 | assertEquals(3, subscriber2.recievedStates.lastOrNull()?.testValue) 71 | 72 | // dereferencing won't remove the subscriber(like in ReSwift) 73 | // subscriber1 = null 74 | store.unsubscribe(subscriber1) 75 | store.dispatch(SetValueAction(5)) 76 | assertEquals(1, store.subscriptions.count()) 77 | assertEquals(5, subscriber2.recievedStates.lastOrNull()?.testValue) 78 | 79 | // dereferencing won't remove the subscriber(like in ReSwift) 80 | // subscriber1 = null 81 | store.unsubscribe(subscriber2) 82 | store.dispatch(SetValueAction(8)) 83 | assertEquals(0, store.subscriptions.count()) 84 | } 85 | 86 | /** 87 | * it replaces the subscription of an existing subscriber with the new one. 88 | */ 89 | @Test 90 | fun testDuplicateSubscription(){ 91 | store = Store(reducer::handleAction, TestAppState()) 92 | val subscriber = TestSubscriber() 93 | 94 | // initial subscription 95 | store.subscribe(subscriber) 96 | // Subsequent subscription that skips repeated updates. 97 | store.subscribe(subscriber) { it.skipRepeats { oldState, newState -> oldState.testValue == newState.testValue } } 98 | 99 | // One initial state update for every subscription. 100 | assertEquals(2, subscriber.recievedStates.count()) 101 | 102 | store.dispatch(SetValueAction(3)) 103 | store.dispatch(SetValueAction(3)) 104 | store.dispatch(SetValueAction(3)) 105 | store.dispatch(SetValueAction(3)) 106 | 107 | assertEquals(3, subscriber.recievedStates.count()) 108 | } 109 | 110 | /** 111 | * it dispatches initial value upon subscription 112 | */ 113 | @Test 114 | fun testDispatchInitialValue() { 115 | store = Store(reducer::handleAction, TestAppState()) 116 | val subscriber = TestSubscriber() 117 | 118 | store.subscribe(subscriber) 119 | store.dispatch(SetValueAction(3)) 120 | 121 | assertEquals(3, subscriber.recievedStates.lastOrNull()?.testValue) 122 | } 123 | 124 | /** 125 | * it allows dispatching from within an observer 126 | */ 127 | @Test 128 | fun testAllowDispatchWithinObserver(){ 129 | store = Store(reducer::handleAction, TestAppState()) 130 | val subscriber = DispatchingSubscriber(store) 131 | 132 | store.subscribe(subscriber) 133 | store.dispatch(SetValueAction(2)) 134 | 135 | assertEquals(5, store.state.testValue) 136 | } 137 | 138 | /** 139 | * it does not dispatch value after subscriber unsubscribes 140 | */ 141 | @Test 142 | fun testDontDispatchToUnsubscribers() { 143 | store = Store(reducer::handleAction, TestAppState()) 144 | val subscriber = TestSubscriber() 145 | 146 | store.dispatch(SetValueAction(5)) 147 | store.subscribe(subscriber) 148 | store.dispatch(SetValueAction(10)) 149 | 150 | store.unsubscribe(subscriber) 151 | // Following value is missed due to not being subscribed: 152 | store.dispatch(SetValueAction(15)) 153 | store.dispatch(SetValueAction(25)) 154 | 155 | store.subscribe(subscriber) 156 | store.dispatch(SetValueAction(20)) 157 | 158 | assertEquals(4, subscriber.recievedStates.count()) 159 | assertEquals(5, subscriber.recievedStates[subscriber.recievedStates.count() - 4].testValue) 160 | assertEquals(10, subscriber.recievedStates[subscriber.recievedStates.count() - 3].testValue) 161 | assertEquals(25, subscriber.recievedStates[subscriber.recievedStates.count() - 2].testValue) 162 | assertEquals(20, subscriber.recievedStates[subscriber.recievedStates.count() - 1].testValue) 163 | } 164 | 165 | /** 166 | * it ignores identical subscribers 167 | */ 168 | @Test 169 | fun testIgnoreIdenticalSubscribers() { 170 | store = Store(reducer::handleAction, TestAppState()) 171 | val subscriber = TestSubscriber() 172 | 173 | store.subscribe(subscriber) 174 | store.subscribe(subscriber) 175 | 176 | assertEquals(1, store.subscriptions.count()) 177 | } 178 | 179 | /** 180 | * it ignores identical subscribers that provide substate selectors 181 | */ 182 | @Test 183 | fun testIgnoreIdenticalSubstateSubscribers() { 184 | store = Store(reducer::handleAction, TestAppState()) 185 | val subscriber = TestSubscriber() 186 | 187 | store.subscribe(subscriber) { it } 188 | store.subscribe(subscriber) { it } 189 | 190 | assertEquals(1, store.subscriptions.count()) 191 | } 192 | } -------------------------------------------------------------------------------- /src/main/kotlin/Subscription.kt: -------------------------------------------------------------------------------- 1 | package tw.geothings.rekotlin 2 | 3 | /** 4 | * Created by Taras Vozniuk on 07/08/2017. 5 | * Copyright © 2017 GeoThings. All rights reserved. 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining 8 | * a copy of this software and associated documentation files (the 9 | * "Software"), to deal in the Software without restriction, including 10 | * without limitation the rights to use, copy, modify, merge, publish, 11 | * distribute, sublicense, and/or sell copies of the Software, and to 12 | * permit persons to whom the Software is furnished to do so, subject to 13 | * the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be 16 | * included in all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 20 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 21 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 22 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 24 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | */ 26 | 27 | /** 28 | * A box around subscriptions and subscribers. 29 | * 30 | * Acts as a type-erasing wrapper around a subscription and its transformed subscription. 31 | * The transformed subscription has a type argument that matches the selected substate of the 32 | * subscriber; however that type cannot be exposed to the store. 33 | * 34 | * The box subscribes either to the original subscription, or if available to the transformed 35 | * subscription and passes any values that come through this subscriptions to the subscriber. 36 | * 37 | */ 38 | class SubscriptionBox(val originalSubscription: Subscription, 39 | transformedSubscription: Subscription?, 40 | val subscriber: StoreSubscriber) where State: StateType { 41 | 42 | // hoping to mimic swift weak reference 43 | // however this doesn't really work the same way, gc collects non-deterministically 44 | // setting the original subscriber to null will not result in this being nulled synchronously 45 | /* 46 | var _subscriber: WeakReference> = WeakReference(subscriber) 47 | val subscriber: StoreSubscriber? 48 | get() { 49 | return _subscriber.get() 50 | } 51 | */ 52 | 53 | init { 54 | // If we haven't received a transformed subscription, we forward all values 55 | // from the original subscription. 56 | val forwardFromOriginalSubscription = { 57 | // original Swift implementation has type errased subscriber 58 | // to avoid casting and passing incompatible value 59 | // conditional cast was added check 60 | originalSubscription.observe { _, newState -> 61 | @Suppress("UNCHECKED_CAST") 62 | (newState as? SelectedState)?.let { 63 | this.subscriber.newState(it) 64 | } 65 | } 66 | } 67 | 68 | // If we received a transformed subscription, we subscribe to that subscription 69 | // and forward all new values to the subscriber. 70 | transformedSubscription?.let { 71 | transformedSubscription.observe { _, newState -> 72 | this.subscriber.newState(newState) 73 | } 74 | 75 | } ?: forwardFromOriginalSubscription() 76 | } 77 | 78 | fun newValues(oldState: State?, newState: State){ 79 | // We pass all new values through the original subscription, which accepts 80 | // values of type ``. If present, transformed subscriptions will 81 | // receive this update and transform it before passing it on to the subscriber. 82 | this.originalSubscription.newValues(oldState, newState) 83 | } 84 | } 85 | 86 | class Subscription { 87 | 88 | private fun _select(selector: ((State) -> Substate)): Subscription { 89 | return Subscription { sink -> 90 | this.observe { oldState, newState -> 91 | sink(oldState?.let(selector), selector(newState)) 92 | } 93 | } 94 | } 95 | 96 | // region: Public interface 97 | 98 | /** 99 | * Provides a subscription that selects a substate of the state of the original subscription. 100 | * @param selector A closure that maps a state to a selected substate 101 | */ 102 | fun select(selector: ((State) -> Substate)): Subscription { 103 | return this._select(selector) 104 | } 105 | 106 | /** 107 | * Provides a subscription that skips certain state updates of the original subscription. 108 | * @param isRepeat A closure that determines whether a given state update is a repeat and 109 | * thus should be skipped and not forwarded to subscribers. 110 | * 111 | */ 112 | fun skipRepeats(isRepeat: (oldState: State, newState: State) -> Boolean): Subscription{ 113 | return Subscription { sink -> 114 | this.observe { oldState, newState -> 115 | oldState?.let { 116 | if (!isRepeat(oldState, newState)){ 117 | sink(oldState, newState) 118 | } 119 | 120 | } ?: sink(oldState, newState) 121 | } 122 | } 123 | } 124 | 125 | /** 126 | * Provides a subscription that skips repeated updates of the original subscription 127 | * Repeated updates determined by structural equality 128 | */ 129 | fun skipRepeats(): Subscription{ 130 | return this.skipRepeats { oldState, newState -> 131 | oldState == newState 132 | } 133 | } 134 | 135 | /** Provides a subscription that skips certain state updates of the original subscription. 136 | * 137 | * This is identical to `skipRepeats` and is provided simply for convenience. 138 | * @param when A closure that determines whether a given state update is a repeat and 139 | * thus should be skipped and not forwarded to subscribers. 140 | */ 141 | fun skip(`when`: (oldState: State, newState: State) -> Boolean): Subscription{ 142 | return this.skipRepeats(`when`) 143 | } 144 | 145 | /** 146 | * Provides a subscription that only updates for certain state changes. 147 | * 148 | * This is effectively the inverse of skipRepeats(:) 149 | * @param whenBlock A closure that determines whether a given state update should notify 150 | */ 151 | fun only(whenBlock: (oldState: State, newState: State) -> Boolean): Subscription{ 152 | return this.skipRepeats { oldState, newState -> 153 | !whenBlock(oldState, newState) 154 | } 155 | } 156 | 157 | // endregion 158 | 159 | // region: Internals 160 | var observer: ((State?, State) -> Unit)? = null 161 | 162 | init {} 163 | constructor() 164 | 165 | /// Initializes a subscription with a sink closure. The closure provides a way to send 166 | /// new values over this subscription. 167 | private constructor(sink: ((State?, State) -> Unit) -> Unit){ 168 | // Provide the caller with a closure that will forward all values 169 | // to observers of this subscription. 170 | 171 | sink { old, new -> 172 | this.newValues(old, new) 173 | } 174 | } 175 | 176 | /** 177 | * Sends new values over this subscription. Observers will be notified of these new values. 178 | */ 179 | fun newValues(oldState: State?, newState: State){ 180 | this.observer?.invoke(oldState, newState) 181 | } 182 | 183 | /// A caller can observe new values of this subscription through the provided closure. 184 | /// - Note: subscriptions only support a single observer. 185 | internal fun observe(observer: (State?, State) -> Unit){ 186 | this.observer = observer 187 | } 188 | 189 | // endregion 190 | } -------------------------------------------------------------------------------- /src/test/kotlin/StoreSubscriberTests.kt: -------------------------------------------------------------------------------- 1 | import org.junit.jupiter.api.Test 2 | import org.junit.jupiter.api.Assertions.* 3 | import tw.geothings.rekotlin.Action 4 | import tw.geothings.rekotlin.StateType 5 | 6 | import tw.geothings.rekotlin.Store 7 | import tw.geothings.rekotlin.StoreSubscriber 8 | import tw.geothings.rekotlin.junit.* 9 | 10 | /** 11 | * Created by Taras Vozniuk on 10/08/2017. 12 | * Copyright © 2017 GeoThings. All rights reserved. 13 | * 14 | * Permission is hereby granted, free of charge, to any person obtaining 15 | * a copy of this software and associated documentation files (the 16 | * "Software"), to deal in the Software without restriction, including 17 | * without limitation the rights to use, copy, modify, merge, publish, 18 | * distribute, sublicense, and/or sell copies of the Software, and to 19 | * permit persons to whom the Software is furnished to do so, subject to 20 | * the following conditions: 21 | * 22 | * The above copyright notice and this permission notice shall be 23 | * included in all copies or substantial portions of the Software. 24 | * 25 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 26 | * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 27 | * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 28 | * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 29 | * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 30 | * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 31 | * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 32 | */ 33 | 34 | internal class StoreSubscriberTests { 35 | 36 | /** 37 | * it allows to pass a state selector closure 38 | */ 39 | @Test 40 | fun testAllowsSelectorClosure(){ 41 | val reducer = TestReducer() 42 | val store = Store(reducer = reducer::handleAction, state = TestAppState()) 43 | val subscriber = TestFilteredSubscriber() 44 | 45 | store.subscribe(subscriber){ 46 | it.select { it.testValue } 47 | } 48 | 49 | store.dispatch(SetValueAction(3)) 50 | 51 | assertEquals(3, subscriber.recievedValue) 52 | 53 | store.dispatch(SetValueAction(null)) 54 | 55 | assertEquals(null, subscriber.recievedValue) 56 | } 57 | 58 | /** 59 | * it supports complex state selector closures 60 | */ 61 | @Test 62 | fun testComplexStateSelector(){ 63 | val reducer = TestComplexAppStateReducer() 64 | val store = Store(reducer = reducer::handleAction, state = TestComplexAppState()) 65 | val subscriber = TestSelectiveSubscriber() 66 | 67 | store.subscribe(subscriber){ 68 | it.select { Pair(it.testValue, it.otherState?.name) } 69 | } 70 | store.dispatch(SetValueAction(5)) 71 | store.dispatch(SetOtherStateAction(OtherState("TestName", 99))) 72 | 73 | assertEquals(5, subscriber.recievedValue.first) 74 | assertEquals("TestName", subscriber.recievedValue.second) 75 | } 76 | 77 | /** 78 | * it does not notify subscriber for unchanged substate state when using `skipRepeats`. 79 | */ 80 | @Test 81 | fun testUnchangedStateSelector() { 82 | val reducer = TestReducer() 83 | val state = TestAppState(3) 84 | val store = Store(reducer = reducer::handleAction, state = state) 85 | val subscriber = TestFilteredSubscriber() 86 | 87 | store.subscribe(subscriber){ 88 | it.select { 89 | it.testValue 90 | }.skipRepeats { oldState, newState -> 91 | oldState == newState 92 | } 93 | } 94 | 95 | assertEquals(3, subscriber.recievedValue) 96 | 97 | store.dispatch(SetValueAction(3)) 98 | 99 | assertEquals(3, subscriber.recievedValue) 100 | assertEquals(1, subscriber.newStateCallCount) 101 | } 102 | 103 | /** 104 | * it does not notify subscriber for unchanged substate state when using the default 105 | * `skipRepeats` implementation. 106 | */ 107 | @Test 108 | fun testUnchangedStateSelectorDefaultSkipRepeats() { 109 | val reducer = TestValueStringReducer() 110 | val state = TestStringAppState() 111 | val store = Store(reducer::handleAction, state) 112 | val subscriber = TestFilteredSubscriber() 113 | 114 | store.subscribe(subscriber){ 115 | it.select { it.testValue }.skipRepeats() 116 | } 117 | 118 | assertEquals("Initial", subscriber.recievedValue) 119 | 120 | store.dispatch(SetValueStringAction("Initial")) 121 | 122 | assertEquals("Initial", subscriber.recievedValue) 123 | assertEquals(1, subscriber.newStateCallCount) 124 | } 125 | 126 | /** 127 | * it skips repeated state values by when `skipRepeats` returns `true`. 128 | */ 129 | @Test 130 | fun testSkipsStateUpdatesForCustomEqualityChecks(){ 131 | val reducer = TestCustomAppStateReducer() 132 | val state = TestCustomAppState(5) 133 | val store = Store(reducer::handleAction, state) 134 | val subscriber = TestFilteredSubscriber() 135 | 136 | store.subscribe(subscriber){ 137 | it.select { it.substate } 138 | .skipRepeats { oldState, newState -> oldState.value == newState.value } 139 | } 140 | 141 | assertEquals(5, subscriber.recievedValue?.value) 142 | 143 | store.dispatch(SetCustomSubstateAction(5)) 144 | 145 | assertEquals(5, subscriber.recievedValue?.value) 146 | assertEquals(1, subscriber.newStateCallCount) 147 | } 148 | 149 | @Test 150 | fun testPassesOnDuplicateSubstateUpdatesByDefault() { 151 | val reducer = TestValueStringReducer() 152 | val state = TestStringAppState() 153 | val store = Store(reducer::handleAction, state) 154 | val subscriber = TestFilteredSubscriber() 155 | 156 | store.subscribe(subscriber) { 157 | it.select { it.testValue } 158 | } 159 | 160 | assertEquals("Initial", subscriber.recievedValue) 161 | 162 | store.dispatch(SetValueStringAction("Initial")) 163 | 164 | assertEquals("Initial", subscriber.recievedValue) 165 | assertEquals(2, subscriber.newStateCallCount) 166 | } 167 | 168 | @Test 169 | fun testSkipsStateUpdatesForEquatableStateByDefault() { 170 | val reducer = TestValueStringReducer() 171 | val state = TestStringAppState() 172 | val store = Store(reducer::handleAction, state) 173 | val subscriber = TestFilteredSubscriber() 174 | 175 | store.subscribe(subscriber) 176 | 177 | assertEquals("Initial", subscriber.recievedValue?.testValue) 178 | 179 | store.dispatch(SetValueStringAction("Initial")) 180 | 181 | assertEquals("Initial", subscriber.recievedValue?.testValue) 182 | assertEquals(1, subscriber.newStateCallCount) 183 | } 184 | 185 | @Test 186 | fun testPassesOnDuplicateStateUpdatesInCustomizedStore() { 187 | val reducer = TestValueStringReducer() 188 | val state = TestStringAppState() 189 | val store = Store(reducer::handleAction, state, automaticallySkipRepeats = false) 190 | val subscriber = TestFilteredSubscriber() 191 | 192 | store.subscribe(subscriber) 193 | 194 | assertEquals("Initial", subscriber.recievedValue?.testValue) 195 | 196 | store.dispatch(SetValueStringAction("Initial")) 197 | 198 | assertEquals("Initial", subscriber.recievedValue?.testValue) 199 | assertEquals(2, subscriber.newStateCallCount) 200 | } 201 | 202 | @Test 203 | fun testSkipWhen(){ 204 | val reducer = TestCustomAppStateReducer() 205 | val state = TestCustomAppState(5) 206 | val store = Store(reducer::handleAction, state) 207 | val subscriber = TestFilteredSubscriber() 208 | 209 | store.subscribe(subscriber){ 210 | it.select { it.substate } 211 | .skip { oldState, newState -> oldState.value == newState.value } 212 | } 213 | 214 | assertEquals(5, subscriber.recievedValue?.value) 215 | 216 | store.dispatch(SetCustomSubstateAction(5)) 217 | 218 | assertEquals(5, subscriber.recievedValue?.value) 219 | assertEquals(1, subscriber.newStateCallCount) 220 | } 221 | 222 | @Test 223 | fun testOnlyWhen(){ 224 | val reducer = TestCustomAppStateReducer() 225 | val state = TestCustomAppState(5) 226 | val store = Store(reducer::handleAction, state) 227 | val subscriber = TestFilteredSubscriber() 228 | 229 | store.subscribe(subscriber){ 230 | it.select { it.substate } 231 | .only { oldState, newState -> oldState.value != newState.value } 232 | } 233 | 234 | assertEquals(5, subscriber.recievedValue?.value) 235 | 236 | store.dispatch(SetCustomSubstateAction(5)) 237 | 238 | assertEquals(5, subscriber.recievedValue?.value) 239 | assertEquals(1, subscriber.newStateCallCount) 240 | } 241 | } 242 | 243 | internal class TestFilteredSubscriber: StoreSubscriber { 244 | var recievedValue: T? = null 245 | var newStateCallCount = 0 246 | 247 | override fun newState(state: T){ 248 | recievedValue = state 249 | newStateCallCount += 1 250 | } 251 | } 252 | 253 | /** 254 | * Example of how you can select a substate. The return value from 255 | *`selectSubstate` and the argument for `newState` need to match up. 256 | */ 257 | class TestSelectiveSubscriber: StoreSubscriber> { 258 | var recievedValue: Pair = Pair(null, null) 259 | 260 | override fun newState(state: Pair) { 261 | recievedValue = state 262 | } 263 | } 264 | 265 | 266 | internal data class TestComplexAppState(val testValue: Int?, val otherState: OtherState?): StateType { 267 | constructor(): this(null, null) 268 | } 269 | internal data class OtherState(val name: String?, val age: Int?) 270 | 271 | internal class TestComplexAppStateReducer { 272 | fun handleAction(action: Action, state: TestComplexAppState?): TestComplexAppState { 273 | var state = state ?: TestComplexAppState() 274 | 275 | when(action){ 276 | is SetValueAction -> { 277 | state = state.copy(testValue = action.value) 278 | } 279 | is SetOtherStateAction -> { 280 | state = state.copy(otherState = action.otherState) 281 | } 282 | } 283 | 284 | return state 285 | } 286 | } 287 | 288 | internal data class SetOtherStateAction(val otherState: OtherState): Action -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## ReKotlin (preview) 2 | 3 | [![License MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://github.com/ReSwift/ReSwift/blob/master/LICENSE.md) 4 | 5 | ### Has been moved to https://github.com/ReKotlin/ReKotlin 6 | 7 | Port of [ReSwift](https://github.com/ReSwift/ReSwift) to Kotlin, which corresponds to [ReSwift/4.0.0](https://github.com/ReSwift/ReSwift/releases/tag/4.0.0) 8 | 9 | ## Introduction 10 | 11 | ReKotlin is a [Redux](https://github.com/reactjs/redux)-like implementation of the unidirectional data flow architecture in Kotlin. ReKotlin helps you to separate three important concerns of your app's components: 12 | 13 | - **State**: in a ReKotlin app the entire app state is explicitly stored in a data structure. This helps avoid complicated state management code, enables better debugging and has many, many more benefits... 14 | - **Views**: in a ReKotlin app your views update when your state changes. Your views become simple visualizations of the current app state. 15 | - **State Changes**: in a ReKotlin app you can only perform state changes through actions. Actions are small pieces of data that describe a state change. By drastically limiting the way state can be mutated, your app becomes easier to understand and it gets easier to work with many collaborators. 16 | 17 | The ReKotlin library is tiny - allowing users to dive into the code, understand every single line and hopefully contribute. 18 | 19 | 20 | ## About ReKotlin 21 | 22 | ReKotlin relies on a few principles: 23 | - **The Store** stores your entire app state in the form of a single data structure. This state can only be modified by dispatching Actions to the store. Whenever the state in the store changes, the store will notify all observers. 24 | - **Actions** are a declarative way of describing a state change. Actions don't contain any code, they are consumed by the store and forwarded to reducers. Reducers will handle the actions by implementing a different state change for each action. 25 | - **Reducers** provide pure functions, that based on the current action and the current app state, create a new app state 26 | 27 | ![](Docs/img/reswift_concept.png) 28 | 29 | For a very simple app, that maintains a counter that can be increased and decreased, you can define the app state as following: 30 | 31 | ```kotlin 32 | data class AppState ( 33 | val counter: Int = 0 34 | ): StateType 35 | ``` 36 | 37 | You would also define two actions, one for increasing and one for decreasing the counter. For the simple actions in this example we can define empty data classes that conform to action: 38 | 39 | ```kotlin 40 | data class CounterActionIncrease(val unit: Unit = Unit): Action 41 | data class CounterActionDecrease(val unit: Unit = Unit): Action 42 | ``` 43 | 44 | Your reducer needs to respond to these different action types, that can be done by switching over the type of action: 45 | 46 | ```kotlin 47 | fun counterReducer(action: Action, state: AppState?): AppState { 48 | // if no state has been provided, create the default state 49 | var state = state ?: AppState() 50 | 51 | when(action){ 52 | is CounterActionIncrease -> { 53 | state = state.copy(counter = state.counter + 1) 54 | } 55 | is CounterActionDecrease -> { 56 | state = state.copy(counter = state.counter - 1) 57 | } 58 | } 59 | 60 | return state 61 | } 62 | ``` 63 | In order to have a predictable app state, it is important that the reducer is always free of side effects, it receives the current app state and an action and returns the new app state. 64 | 65 | To maintain our state and delegate the actions to the reducers, we need a store. Let's call it `mainStore` and define it as a global constant, for example in the Main Activity file: 66 | 67 | ```kotlin 68 | val mainStore = Store( 69 | reducer = ::counterReducer, 70 | state = null 71 | ) 72 | 73 | class MainActivity : AppCompatActivity(){ 74 | //... 75 | } 76 | ``` 77 | 78 | 79 | Lastly, your view layer, in this case an activity, needs to tie into this system by subscribing to store updates and emitting actions whenever the app state needs to be changed: 80 | 81 | ```kotlin 82 | class MainActivity : AppCompatActivity(), StoreSubscriber { 83 | 84 | private val counterLabel: TextView by lazy { 85 | this.findViewById(R.id.counter_label) as TextView 86 | } 87 | 88 | private val buttonUp: Button by lazy { 89 | this.findViewById(R.id.button) as Button 90 | } 91 | 92 | private val buttonDown: Button by lazy { 93 | this.findViewById(R.id.button2) as Button 94 | } 95 | 96 | override fun onCreate(savedInstanceState: Bundle?) { 97 | super.onCreate(savedInstanceState) 98 | setContentView(R.layout.activity_main) 99 | 100 | // when either button is tapped, an action is dispatched to the store 101 | // in order to update the application state 102 | this.buttonUp.setOnClickListener { 103 | mainStore.dispatch(CounterActionIncrease()) 104 | } 105 | this.buttonDown.setOnClickListener { 106 | mainStore.dispatch(CounterActionDecrease()) 107 | } 108 | 109 | // subscribe to state changes 110 | mainStore.subscribe(this) 111 | } 112 | 113 | override fun newState(state: AppState) { 114 | // when the state changes, the UI is updated to reflect the current state 115 | this.counterLabel.text = "${state.counter}" 116 | } 117 | } 118 | ``` 119 | 120 | The `newState` method will be called by the `Store` whenever a new app state is available, this is where we need to adjust our view to reflect the latest app state. 121 | 122 | Button taps result in dispatched actions that will be handled by the store and its reducers, resulting in a new app state. 123 | 124 | This is a very basic example that only shows a subset of ReKotlin's features, read the Getting Started Guide __(not ported yet)__ to see how you can build entire apps with this architecture. For a complete implementation of this example see the [ReKotlin-CounterExample](https://github.com/GeoThings/ReKotlin-CounterExample) project. 125 | 126 | [You can also watch this talk on the motivation behind the original ReSwift](https://realm.io/news/benji-encz-unidirectional-data-flow-swift/). 127 | 128 | ## Why ReKotlin? 129 | 130 | Model-View-Controller (MVC) is not a holistic application architecture. Typical apps defer a lot of complexity to controllers since MVC doesn't offer other solutions for state management, one of the most complex issues in app development. 131 | 132 | Apps built upon MVC often end up with a lot of complexity around state management and propagation. We need to use callbacks, delegations, Key-Value-Observation and notifications to pass information around in our apps and to ensure that all the relevant views have the latest state. 133 | 134 | This approach involves a lot of manual steps and is thus error prone and doesn't scale well in complex code bases. 135 | 136 | It also leads to code that is difficult to understand at a glance, since dependencies can be hidden deep inside of view controllers. Lastly, you mostly end up with inconsistent code, where each developer uses the state propagation procedure they personally prefer. You can circumvent this issue by style guides and code reviews but you cannot automatically verify the adherence to these guidelines. 137 | 138 | ReKotlin attempts to solve these problem by placing strong constraints on the way applications can be written. This reduces the room for programmer error and leads to applications that can be easily understood - by inspecting the application state data structure, the actions and the reducers. 139 | 140 | This architecture provides further benefits beyond improving your code base: 141 | 142 | - Stores, Reducers, Actions and extensions such as ReSwift Router __(port not yet available)__ are entirely platform independent - you can easily use the same business logic and share it between apps for multiple platforms 143 | - Want to collaborate with a co-worker on fixing an app crash? Use __(port not yet available)__ [ReSwift Recorder](https://github.com/ReSwift/ReSwift-Recorder) to record the actions that lead up to the crash and send them the JSON file so that they can replay the actions and reproduce the issue right away. 144 | - Maybe recorded actions can be used to build UI and integration tests? 145 | 146 | The ReKotlin tooling is still in a very early stage, but aforementioned prospects excite us and hopefully others in the community as well! 147 | 148 | ## Getting Started Guide 149 | 150 | Getting started guide has not yet been ported. In the meantime, please refer to original ReSwift's: 151 | [Getting Started Guide that describes the core components of apps built with ReSwift](http://reswift.github.io/ReSwift/master/getting-started-guide.html). 152 | 153 | To get an understanding of the core principles we recommend reading the brilliant [redux documentation](http://redux.js.org/). 154 | 155 | ## Installation 156 | 157 | We are still in preview here, so the package is not yet available at maven central repository. Instead please use our development repository: 158 | 159 | ```gradle 160 | // build.gradle 161 | repositories { 162 | maven { 163 | url 'https://rekotlin.s3-ap-southeast-1.amazonaws.com/snapshots' 164 | } 165 | } 166 | 167 | dependencies { 168 | compile 'tw.geothings.rekotlin:rekotlin:0.1.0-SNAPSHOT' 169 | } 170 | 171 | ``` 172 | 173 | ## Differences with ReSwift 174 | 175 | ### Dereferencing subscribers will not result in subscription removed 176 | 177 | In ReSwift when you dereference the subscriber or it goes out of the scope, you won't receive new state updates. 178 | 179 | ```swift 180 | var subscriber: TestSubscriber? = TestSubscriber() 181 | store.subscribe(subscriber!) 182 | subscriber = nil 183 | ``` 184 | 185 | However in ReKotlin you need make sure you have unsubscribed explicitly. 186 | 187 | ```kotlin 188 | val subscriber = TestSubscriber() 189 | store.subscribe(subscriber) 190 | store.unsubscribe(subscriber) 191 | ``` 192 | 193 | ### Equatability and skipRepeats 194 | 195 | When subscribing without substate selection like `store.subscribe(someSubscriber)` in swift you need to have your state implementing Equatable in order to skipRepeats being applied automatically. 196 | 197 | ```swift 198 | public struct State: StateType { 199 | public let mapState: MapState 200 | public let appState: AppState 201 | } 202 | 203 | extension State: Equatable { 204 | public static func ==(lhs: State, rhs: State) -> Bool { 205 | //... 206 | } 207 | } 208 | 209 | ``` 210 | 211 | However in Kotlin(JVM) every object implements `equals()`, so that skipRepeats will be applied automatically when you `store.subscribe(someSubscriber)`, with Kotlin [Structural Equality](https://kotlinlang.org/docs/reference/equality.html#structural-equality) check used. 212 | 213 | Please note, if you implement your states/substates with [data classes](https://kotlinlang.org/docs/reference/data-classes.html), Kotlin compiler will automatically derive non-shallow `equals()` from all properties declared in the primary constructor. 214 | 215 | If you want to opt-out of this behaviour please set `automaticallySkipRepeats` to __false__ in your store declaration: 216 | 217 | ```kotlin 218 | val store = Store( 219 | reducer::handleAction, 220 | state, 221 | automaticallySkipRepeats = false) 222 | ``` 223 | 224 | ## Credits 225 | 226 | - Many thanks to [Benjamin Encz](https://github.com/Ben-G) and other ReSwift contributors for buidling original [ReSwift](https://github.com/ReSwift/ReSwift) that we really enjoyed working with. 227 | - Also huge thanks to [Dan Abramov](https://github.com/gaearon) for building [Redux](https://github.com/reactjs/redux) - all ideas in here and many implementation details were provided by his library. 228 | 229 | --------------------------------------------------------------------------------