├── .gitignore
├── .travis.yml
├── LICENSE
├── api
├── build.gradle
└── src
│ └── main
│ └── java
│ └── redux
│ └── api
│ ├── Dispatcher.java
│ ├── Reducer.java
│ ├── Store.java
│ └── enhancer
│ └── Middleware.java
├── build.gradle
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle
└── specs
├── build.gradle
└── src
└── main
└── java
└── redux
└── api
├── StoreTest.java
└── helpers
├── Actions.java
├── ActionsCreator.java
├── Reducers.java
├── State.java
└── Todo.java
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .idea
3 | build/
4 |
5 | # Ignore Gradle GUI config
6 | gradle-app.setting
7 |
8 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
9 | !gradle-wrapper.jar
10 |
11 | # Cache of project
12 | .gradletasknamecache
13 |
14 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
15 | # gradle/wrapper/gradle-wrapper.properties
16 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: java
2 |
3 | before_cache:
4 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
5 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/
6 |
7 | cache:
8 | directories:
9 | - $HOME/.gradle/caches/
10 | - $HOME/.gradle/wrapper/
11 |
12 | script: ./gradlew build
13 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2016 jvm-redux
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 |
--------------------------------------------------------------------------------
/api/build.gradle:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jvm-redux/jvm-redux-api/91ffd733953078ae6059e64039c73b7b616fbd95/api/build.gradle
--------------------------------------------------------------------------------
/api/src/main/java/redux/api/Dispatcher.java:
--------------------------------------------------------------------------------
1 | package redux.api;
2 |
3 | /**
4 | * A dispatcher is an interface that accepts an action or an async action; it then may or may not dispatch one or more
5 | * actions to the store.
6 | *
7 | * @see http://redux.js.org/docs/Glossary.html#dispatching-function
8 | */
9 | public interface Dispatcher {
10 |
11 | /**
12 | * Dispatches an action. This is the only way to trigger a state change.
13 | *
14 | * @param action A plain object describing the change that makes sense for your application
15 | * @return The dispatched action
16 | * @see http://redux.js.org/docs/api/Store.html#dispatch
17 | */
18 | Object dispatch(Object action);
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/api/src/main/java/redux/api/Reducer.java:
--------------------------------------------------------------------------------
1 | package redux.api;
2 |
3 | /**
4 | * A reducer accepts an accumulation and a value and returns a new accumulation. They are used to reduce a collection of
5 | * values down to a single value.
6 | *
7 | * @see http://redux.js.org/docs/basics/Reducers.html
8 | */
9 | public interface Reducer {
10 |
11 | /**
12 | * A pure function which returns a new state given the previous state and an action.
13 | *
14 | * Things you should never do inside a reducer:
15 | *
16 | * - Mutate its arguments
17 | * - Perform side effects like API calls and routing transitions
18 | * - Call non-pure functions, e.g. Date() or Random.nextInt()
19 | *
20 | *
21 | * Given the same arguments, it should calculate the next state and return it. No surprises. No side effects. No API
22 | * calls. No mutations. Just a calculation.
23 | *
24 | * @param state The previous state
25 | * @param action The dispatched action
26 | * @return The new state
27 | * @see http://redux.js.org/docs/basics/Reducers.html
28 | */
29 | S reduce(S state, Object action);
30 |
31 | }
32 |
--------------------------------------------------------------------------------
/api/src/main/java/redux/api/Store.java:
--------------------------------------------------------------------------------
1 | package redux.api;
2 |
3 | /**
4 | * Coordinates actions and {@link Reducer}. Store has the following responsibilities:
5 | *
6 | * - Holds application state
7 | * - Allows access to state via {@link #getState()}
8 | * - Allows state to be updated via {@link Dispatcher#dispatch(Object)}
9 | * - Registers listeners via {@link #subscribe(Subscriber)}
10 | * - Handles unregistering of listeners via the {@link Subscriber} returned by {@link #subscribe(Subscriber)}
11 | *
12 | *
13 | * @see http://redux.js.org/docs/basics/Store.html
14 | */
15 | public interface Store extends Dispatcher {
16 |
17 | /**
18 | * Initialization action reference.
19 | */
20 | Init INIT = Init.INSTANCE;
21 |
22 | /**
23 | * Returns the current state tree of your application. It is equal to the last value returned by the store’s
24 | * reducer.
25 | *
26 | * @return the current state
27 | * @see http://redux.js.org/docs/api/Store.html#getState
28 | */
29 | S getState();
30 |
31 | /**
32 | * Adds a change listener. It will be called any time an action is dispatched, and some part of the state tree may
33 | * potentially have changed. You may then call {@link #getState()} to read the current state tree inside the
34 | * callback.
35 | *
36 | * @param subscriber The subscriber
37 | * @return A subscription
38 | * @see http://redux.js.org/docs/api/Store.html#subscribe
39 | */
40 | Subscription subscribe(Subscriber subscriber);
41 |
42 | /**
43 | * Replaces the reducer currently used by the store to calculate the state.
44 | *
45 | * @param reducer The reducer
46 | * @see http://redux.js.org/docs/api/Store.html#replaceReducer
47 | */
48 | void replaceReducer(Reducer reducer);
49 |
50 | /**
51 | * An interface that creates a store.
52 | *
53 | * @see http://redux.js.org/docs/Glossary.html#store-creator
54 | */
55 | interface Creator {
56 |
57 | /**
58 | * Creates a store.
59 | *
60 | * @param reducer Reducer for the store state
61 | * @param initialState Initial state for the store
62 | * @return The store
63 | */
64 | Store create(Reducer reducer, S initialState);
65 |
66 | }
67 |
68 | /**
69 | * An interface that composes a store creator to return a new, enhanced store creator.
70 | *
71 | * @see http://redux.js.org/docs/Glossary.html#store-enhancer
72 | */
73 | interface Enhancer {
74 |
75 | /**
76 | * Composes a store creator to return a new, enhanced store creator.
77 | *
78 | * @param next The next store creator to compose
79 | * @return The composed store creator
80 | */
81 | Creator enhance(Creator next);
82 |
83 | }
84 |
85 | /**
86 | * A listener which will be called any time an action is dispatched, and some part of the state tree may potentially
87 | * have changed. You may then call {@link #getState()} to read the current state tree inside the listener.
88 | *
89 | * @see http://redux.js.org/docs/api/Store.html#subscribe
90 | */
91 | interface Subscriber {
92 |
93 | /**
94 | * Called any time an action is dispatched.
95 | */
96 | void onStateChanged();
97 |
98 | }
99 |
100 | /**
101 | * A reference to the {@link Subscriber} to allow for unsubscription.
102 | */
103 | interface Subscription {
104 |
105 | Subscription EMPTY = new Subscription() {
106 | @Override
107 | public void unsubscribe() {
108 | }
109 | };
110 |
111 | /**
112 | * Unsubscribe the {@link Subscriber} from the {@link Store}.
113 | */
114 | void unsubscribe();
115 |
116 | }
117 |
118 | /**
119 | * Initialization action.
120 | */
121 | public final class Init {
122 |
123 | private static final Init INSTANCE = new Init();
124 |
125 | private Init() {}
126 |
127 | }
128 |
129 | }
130 |
--------------------------------------------------------------------------------
/api/src/main/java/redux/api/enhancer/Middleware.java:
--------------------------------------------------------------------------------
1 | package redux.api.enhancer;
2 |
3 | import redux.api.Dispatcher;
4 | import redux.api.Store;
5 |
6 | /**
7 | * A middleware is an interface that composes a {@link Dispatcher} to return a new dispatch function. It often turns
8 | * async actions into actions.
9 | *
10 | * @param The store type
11 | * @see http://redux.js.org/docs/advanced/Middleware.html
12 | */
13 | public interface Middleware {
14 |
15 | /**
16 | * Dispatches an action. This is the only way to trigger a state change.
17 | *
18 | * @param store An interface to return the current state of the store.
19 | * @param next The next dispatcher in the chain
20 | * @param action The action
21 | * @return The action
22 | * @see http://redux.js.org/docs/Glossary.html#middleware
23 | */
24 | Object dispatch(Store store, Dispatcher next, Object action);
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/build.gradle:
--------------------------------------------------------------------------------
1 | allprojects {
2 | version = "1.2.0"
3 | }
4 |
5 | subprojects {
6 | apply plugin: 'java'
7 | apply plugin: 'maven'
8 |
9 | sourceCompatibility = JavaVersion.VERSION_1_7
10 | targetCompatibility = JavaVersion.VERSION_1_7
11 |
12 | task sourcesJar(type: Jar, dependsOn: classes) {
13 | classifier = 'sources'
14 | from sourceSets.main.allSource
15 | }
16 |
17 | task javadocJar(type: Jar, dependsOn: javadoc) {
18 | classifier = 'javadoc'
19 | from javadoc.destinationDir
20 | }
21 |
22 | artifacts {
23 | archives sourcesJar
24 | archives javadocJar
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/jvm-redux/jvm-redux-api/91ffd733953078ae6059e64039c73b7b616fbd95/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Thu Oct 13 10:06:11 EDT 2016
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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | include 'api'
2 | include 'specs'
3 |
--------------------------------------------------------------------------------
/specs/build.gradle:
--------------------------------------------------------------------------------
1 | repositories {
2 | mavenCentral()
3 | }
4 |
5 | dependencies {
6 | compile project(":api")
7 |
8 | compile 'junit:junit:4.12'
9 | compile 'org.assertj:assertj-core:1.7.1'
10 | compile 'org.mockito:mockito-core:2.2.16'
11 | compile 'org.jetbrains:annotations:13.0'
12 | }
13 |
--------------------------------------------------------------------------------
/specs/src/main/java/redux/api/StoreTest.java:
--------------------------------------------------------------------------------
1 | package redux.api;
2 |
3 | import org.jetbrains.annotations.NotNull;
4 | import org.junit.Test;
5 | import redux.api.helpers.Reducers;
6 | import redux.api.helpers.State;
7 | import redux.api.helpers.Todo;
8 |
9 | import java.util.ArrayList;
10 | import java.util.List;
11 |
12 | import static org.assertj.core.api.Assertions.assertThat;
13 | import static org.mockito.Mockito.*;
14 | import static redux.api.helpers.ActionsCreator.addTodo;
15 | import static redux.api.helpers.ActionsCreator.unknownAction;
16 |
17 | public abstract class StoreTest {
18 |
19 | @NotNull public abstract Store createStore(@NotNull Reducer reducer, @NotNull S state);
20 |
21 | @Test
22 | public void passesTheInitialActionAndTheInitialState() {
23 | final State initialState = new State(new Todo(1, "Hello"));
24 | final Store store = createStore(Reducers.TODOS, initialState);
25 |
26 | assertThat(store.getState()).isEqualTo(initialState);
27 | }
28 |
29 | @Test
30 | public void appliesTheReducerToThePreviousState() {
31 | final Store store = createStore(Reducers.TODOS, new State());
32 | assertThat(store.getState()).isEqualTo(new State());
33 |
34 | store.dispatch(unknownAction());
35 | assertThat(store.getState()).isEqualTo(new State());
36 |
37 | store.dispatch(addTodo("Hello"));
38 | assertThat(store.getState()).isEqualTo(new State(new Todo(1, "Hello")));
39 |
40 | store.dispatch(addTodo("World"));
41 | assertThat(store.getState()).isEqualTo(new State(
42 | new Todo(1, "Hello"),
43 | new Todo(2, "World")
44 | ));
45 | }
46 |
47 |
48 | @Test
49 | public void appliesTheReducerToTheInitialState() {
50 | final Store store = createStore(Reducers.TODOS, new State(new Todo(1, "Hello")));
51 |
52 | assertThat(store.getState()).isEqualTo(new State(new Todo(1, "Hello")));
53 |
54 | store.dispatch(unknownAction());
55 | assertThat(store.getState()).isEqualTo(new State(new Todo(1, "Hello")));
56 |
57 | store.dispatch(addTodo("World"));
58 | assertThat(store.getState()).isEqualTo(new State(
59 | new Todo(1, "Hello"),
60 | new Todo(2, "World"))
61 | );
62 | }
63 |
64 | @Test
65 | public void sendsInitWhenReplacingAReducer() {
66 | final Store store = createStore(Reducers.TODOS, new State());
67 |
68 | final StubbedStateReducer reducer = new StubbedStateReducer();
69 | store.replaceReducer(reducer);
70 | assertThat(reducer.receivedAction).isEqualTo(redux.api.Store.INIT);
71 | }
72 |
73 | @Test
74 | public void preservesTheStateWhenReplacingAReducer() {
75 | final Store store = createStore(Reducers.TODOS, new State());
76 | store.dispatch(addTodo("Hello"));
77 | store.dispatch(addTodo("World"));
78 | assertThat(store.getState()).isEqualTo(new State(
79 | new Todo(1, "Hello"),
80 | new Todo(2, "World"))
81 | );
82 |
83 | store.replaceReducer(Reducers.TODOS_REVERSE);
84 | assertThat(store.getState()).isEqualTo(new State(
85 | new Todo(1, "Hello"),
86 | new Todo(2, "World"))
87 | );
88 |
89 | store.dispatch(addTodo("Perhaps"));
90 | assertThat(store.getState()).isEqualTo(new State(
91 | new Todo(3, "Perhaps"),
92 | new Todo(1, "Hello"),
93 | new Todo(2, "World"))
94 | );
95 |
96 | store.replaceReducer(Reducers.TODOS);
97 | assertThat(store.getState()).isEqualTo(new State(
98 | new Todo(3, "Perhaps"),
99 | new Todo(1, "Hello"),
100 | new Todo(2, "World"))
101 | );
102 |
103 | store.dispatch(addTodo("Surely"));
104 | assertThat(store.getState()).isEqualTo(new State(
105 | new Todo(3, "Perhaps"),
106 | new Todo(1, "Hello"),
107 | new Todo(2, "World"),
108 | new Todo(4, "Surely"))
109 | );
110 | }
111 |
112 | @Test
113 | public void supportsMultipleSubscriptions() {
114 | final Store store = createStore(Reducers.TODOS, new State());
115 | final Store.Subscriber subscriberA = mock(Store.Subscriber.class);
116 | final Store.Subscriber subscriberB = mock(Store.Subscriber.class);
117 | final Store.Subscription subscriptionA = store.subscribe(subscriberA);
118 |
119 | store.dispatch(unknownAction());
120 | verify(subscriberA, times(1)).onStateChanged();
121 | verify(subscriberB, times(0)).onStateChanged();
122 |
123 | store.dispatch(unknownAction());
124 | verify(subscriberA, times(2)).onStateChanged();
125 | verify(subscriberB, times(0)).onStateChanged();
126 |
127 | final Store.Subscription subscriptionB = store.subscribe(subscriberB);
128 | store.dispatch(unknownAction());
129 | verify(subscriberA, times(3)).onStateChanged();
130 | verify(subscriberB, times(1)).onStateChanged();
131 |
132 | subscriptionA.unsubscribe();
133 | verify(subscriberA, times(3)).onStateChanged();
134 | verify(subscriberB, times(1)).onStateChanged();
135 |
136 | store.dispatch(unknownAction());
137 | verify(subscriberA, times(3)).onStateChanged();
138 | verify(subscriberB, times(2)).onStateChanged();
139 |
140 | subscriptionB.unsubscribe();
141 | verify(subscriberA, times(3)).onStateChanged();
142 | verify(subscriberB, times(2)).onStateChanged();
143 |
144 | store.dispatch(unknownAction());
145 | verify(subscriberA, times(3)).onStateChanged();
146 | verify(subscriberB, times(2)).onStateChanged();
147 |
148 | store.subscribe(subscriberA);
149 | verify(subscriberA, times(3)).onStateChanged();
150 | verify(subscriberB, times(2)).onStateChanged();
151 |
152 | store.dispatch(unknownAction());
153 | verify(subscriberA, times(4)).onStateChanged();
154 | verify(subscriberB, times(2)).onStateChanged();
155 | }
156 |
157 | @Test
158 | public void onlyRemovesSubscriberOnceWhenUnsubscribeIsCalled() {
159 | final Store store = createStore(Reducers.TODOS, new State());
160 | final Store.Subscriber subscriberA = mock(Store.Subscriber.class);
161 | final Store.Subscriber subscriberB = mock(Store.Subscriber.class);
162 |
163 | final Store.Subscription subscriptionA = store.subscribe(subscriberA);
164 | store.subscribe(subscriberB);
165 | subscriptionA.unsubscribe();
166 | subscriptionA.unsubscribe();
167 |
168 | store.dispatch(unknownAction());
169 |
170 | verify(subscriberA, times(0)).onStateChanged();
171 | verify(subscriberB, times(1)).onStateChanged();
172 | }
173 |
174 | @Test
175 | public void onlyRemovesRelevantSubscriberWhenUnsubscribeIsCalled() {
176 | final Store store = createStore(Reducers.TODOS, new State());
177 | final SubbedSubscriber subscriber = new SubbedSubscriber();
178 |
179 | store.subscribe(subscriber);
180 | final Store.Subscription secondSubscription = store.subscribe(subscriber);
181 |
182 | secondSubscription.unsubscribe();
183 |
184 | store.dispatch(unknownAction());
185 | assertThat(subscriber.nbOnStateChangedCall).isEqualTo(1);
186 | }
187 |
188 | @Test
189 | public void supportsRemovingASubscriptionWithinASubscription() {
190 | final Store store = createStore(Reducers.TODOS, new State());
191 | final Store.Subscriber subscriberA = mock(Store.Subscriber.class);
192 | final Store.Subscriber subscriberB = mock(Store.Subscriber.class);
193 | final Store.Subscriber subscriberC = mock(Store.Subscriber.class);
194 |
195 | store.subscribe(subscriberA);
196 | final OneTimeProxySubscriber subscriber = new OneTimeProxySubscriber(subscriberB);
197 | final Store.Subscription subscriptionB = store.subscribe(subscriber);
198 | subscriber.setCurrentSubscription(subscriptionB);
199 |
200 | store.subscribe(subscriberC);
201 | store.dispatch(unknownAction());
202 | store.dispatch(unknownAction());
203 |
204 | verify(subscriberA, times(2)).onStateChanged();
205 | verify(subscriberB, times(1)).onStateChanged();
206 | verify(subscriberC, times(2)).onStateChanged();
207 | }
208 |
209 | private static class OneTimeProxySubscriber implements Store.Subscriber {
210 | private final Store.Subscriber subscriberB;
211 | private Store.Subscription subscriptionB;
212 |
213 | OneTimeProxySubscriber(Store.Subscriber subscriberB) {
214 | this.subscriberB = subscriberB;
215 | }
216 |
217 | void setCurrentSubscription(Store.Subscription subscriptionB) {
218 | this.subscriptionB = subscriptionB;
219 | }
220 |
221 | @Override
222 | public void onStateChanged() {
223 | subscriberB.onStateChanged();
224 | subscriptionB.unsubscribe();
225 | }
226 | }
227 |
228 | @Test
229 | public void delaysUnsubscribeUntilTheEndOfCurrentDispatch() {
230 | final Store store = createStore(Reducers.TODOS, new State());
231 |
232 | final List subscriptions = new ArrayList<>();
233 |
234 | final Store.Subscriber subscriber1 = mock(Store.Subscriber.class);
235 | final Store.Subscriber subscriber2 = mock(Store.Subscriber.class);
236 | final Store.Subscriber subscriber3 = mock(Store.Subscriber.class);
237 |
238 | subscriptions.add(store.subscribe(subscriber1));
239 | subscriptions.add(store.subscribe(new Store.Subscriber() {
240 | @Override
241 | public void onStateChanged() {
242 | subscriber2.onStateChanged();
243 | for (Store.Subscription subscription : subscriptions) {
244 | subscription.unsubscribe();
245 | }
246 | }
247 | }));
248 |
249 | subscriptions.add(store.subscribe(subscriber3));
250 |
251 | store.dispatch(unknownAction());
252 | verify(subscriber1, times(1)).onStateChanged();
253 | verify(subscriber2, times(1)).onStateChanged();
254 | verify(subscriber3, times(1)).onStateChanged();
255 |
256 | store.dispatch(unknownAction());
257 | verify(subscriber1, times(1)).onStateChanged();
258 | verify(subscriber2, times(1)).onStateChanged();
259 | verify(subscriber3, times(1)).onStateChanged();
260 | }
261 |
262 | @Test
263 | public void delaysSubscribeUntilTheEndOfCurrentDispatch() {
264 | final Store store = createStore(Reducers.TODOS, new State());
265 |
266 | final Store.Subscriber subscriber1 = mock(Store.Subscriber.class);
267 | final Store.Subscriber subscriber2 = mock(Store.Subscriber.class);
268 | final Store.Subscriber subscriber3 = mock(Store.Subscriber.class);
269 |
270 | store.subscribe(subscriber1);
271 | store.subscribe(new Store.Subscriber() {
272 | boolean subscriber3Added = false;
273 |
274 | @Override
275 | public void onStateChanged() {
276 | subscriber2.onStateChanged();
277 | if (!subscriber3Added) {
278 | subscriber3Added = true;
279 | store.subscribe(subscriber3);
280 | }
281 | }
282 | }
283 | );
284 |
285 | store.dispatch(unknownAction());
286 | verify(subscriber1, times(1)).onStateChanged();
287 | verify(subscriber2, times(1)).onStateChanged();
288 | verify(subscriber3, times(0)).onStateChanged();
289 |
290 | store.dispatch(unknownAction());
291 | verify(subscriber1, times(2)).onStateChanged();
292 | verify(subscriber2, times(2)).onStateChanged();
293 | verify(subscriber3, times(1)).onStateChanged();
294 | }
295 |
296 |
297 | @Test
298 | public void usesTheLastSnapshotOfSubscribersDuringNestedDispatch() {
299 | final Store store = createStore(Reducers.TODOS, new State());
300 |
301 | final Store.Subscriber subscriber1 = mock(Store.Subscriber.class);
302 | final Store.Subscriber subscriber2 = mock(Store.Subscriber.class);
303 | final Store.Subscriber subscriber3 = mock(Store.Subscriber.class);
304 | final Store.Subscriber subscriber4 = mock(Store.Subscriber.class);
305 |
306 | class Subscriber implements Store.Subscriber {
307 | Store.Subscription subscription1 = Store.Subscription.EMPTY;
308 | Store.Subscription subscription4 = Store.Subscription.EMPTY;
309 |
310 | Subscriber() {
311 | subscription1 = store.subscribe(this);
312 | store.subscribe(subscriber2);
313 | store.subscribe(subscriber3);
314 | }
315 |
316 | @Override
317 | public void onStateChanged() {
318 | subscriber1.onStateChanged();
319 | verify(subscriber1, times(1)).onStateChanged();
320 | verify(subscriber2, times(0)).onStateChanged();
321 | verify(subscriber3, times(0)).onStateChanged();
322 | verify(subscriber4, times(0)).onStateChanged();
323 |
324 | subscription1.unsubscribe();
325 | subscription4 = store.subscribe(subscriber4);
326 | store.dispatch(unknownAction());
327 |
328 | verify(subscriber1, times(1)).onStateChanged();
329 | verify(subscriber2, times(1)).onStateChanged();
330 | verify(subscriber3, times(1)).onStateChanged();
331 | verify(subscriber4, times(1)).onStateChanged();
332 | }
333 | }
334 |
335 | final Subscriber subscriber = new Subscriber();
336 |
337 | store.dispatch(unknownAction());
338 | verify(subscriber1, times(1)).onStateChanged();
339 | verify(subscriber2, times(2)).onStateChanged();
340 | verify(subscriber3, times(2)).onStateChanged();
341 | verify(subscriber4, times(1)).onStateChanged();
342 |
343 | subscriber.subscription4.unsubscribe();
344 | store.dispatch(unknownAction());
345 | verify(subscriber1, times(1)).onStateChanged();
346 | verify(subscriber2, times(3)).onStateChanged();
347 | verify(subscriber3, times(3)).onStateChanged();
348 | verify(subscriber4, times(1)).onStateChanged();
349 | }
350 |
351 | @Test
352 | public void providesAnUptodateStateWhenASubscriberIsNotified() {
353 | final Store store = createStore(Reducers.TODOS, new State());
354 | store.subscribe(new Store.Subscriber() {
355 | @Override
356 | public void onStateChanged() {
357 | assertThat(store.getState()).isEqualTo(new State(new Todo(1, "Hello")));
358 | }
359 | });
360 | store.dispatch(addTodo("Hello"));
361 | }
362 |
363 | @Test
364 | public void handlesNestedDispatchesGracefully() {
365 | final Reducer foo = new Reducer() {
366 | @Override
367 | public Integer reduce(Integer state, Object action) {
368 | if ("foo".equals(action)) {
369 | return 1;
370 | } else {
371 | return state;
372 | }
373 | }
374 | };
375 |
376 | final Reducer bar = new Reducer() {
377 | @Override
378 | public Integer reduce(Integer state, Object action) {
379 | if ("bar".equals(action)) {
380 | return 2;
381 | } else {
382 | return state;
383 | }
384 | }
385 | };
386 |
387 | class CombinedStates {
388 | final int foo;
389 | final int bar;
390 |
391 | CombinedStates(int foo, int bar) {
392 | this.foo = foo;
393 | this.bar = bar;
394 | }
395 | }
396 |
397 | final Reducer combineReducers = new Reducer() {
398 | @Override
399 | public CombinedStates reduce(CombinedStates state, Object action) {
400 | return new CombinedStates(
401 | foo.reduce(state.foo, action),
402 | bar.reduce(state.bar, action)
403 | );
404 | }
405 | };
406 |
407 | final Store store = createStore(combineReducers, new CombinedStates(0, 0));
408 |
409 | final Store.Subscriber subscriberDispatching = new Store.Subscriber() {
410 | @Override
411 | public void onStateChanged() {
412 | CombinedStates state = store.getState();
413 | if (state.bar == 0) {
414 | store.dispatch("bar");
415 | }
416 | }
417 | };
418 |
419 | store.subscribe(subscriberDispatching);
420 | store.dispatch("foo");
421 |
422 | assertThat(store.getState().foo).isEqualTo(1);
423 | assertThat(store.getState().bar).isEqualTo(2);
424 | }
425 |
426 | @Test(expected = Throwable.class)
427 | public void doesNotAllowDispatchFromWithinAReducer() {
428 | class DispatchInMiddle {
429 | final Store store;
430 |
431 | DispatchInMiddle(Store store) {
432 | this.store = store;
433 | }
434 |
435 | void dispatch() {
436 | store.dispatch("DispatchInMiddle");
437 | }
438 | }
439 |
440 | final Store store = createStore(new Reducer() {
441 | @Override
442 | public Integer reduce(Integer state, Object action) {
443 | if (action instanceof DispatchInMiddle) {
444 | ((DispatchInMiddle) action).dispatch();
445 | }
446 | return state;
447 | }
448 | }, 0);
449 |
450 | store.dispatch(new DispatchInMiddle(store));
451 | }
452 |
453 | @Test
454 | public void doesNotThrowIfActionTypeIsUnknown() {
455 | final Store store = createStore(Reducers.TODOS, new State());
456 |
457 | assertThat(store.dispatch("unknown")).isEqualTo("unknown");
458 | }
459 |
460 | private static class StubbedStateReducer implements Reducer {
461 | private Object receivedAction;
462 |
463 | @Override
464 | public State reduce(State state, Object action) {
465 | receivedAction = action;
466 | return state;
467 | }
468 | }
469 |
470 | private class SubbedSubscriber implements Store.Subscriber {
471 | private int nbOnStateChangedCall = 0;
472 |
473 | SubbedSubscriber() {
474 | }
475 |
476 | @Override
477 | public void onStateChanged() {
478 | nbOnStateChangedCall++;
479 | }
480 | }
481 | }
482 |
--------------------------------------------------------------------------------
/specs/src/main/java/redux/api/helpers/Actions.java:
--------------------------------------------------------------------------------
1 | package redux.api.helpers;
2 |
3 | class Actions {
4 |
5 | static class AddTodo {
6 | final String message;
7 |
8 | AddTodo(String message) {
9 | this.message = message;
10 | }
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/specs/src/main/java/redux/api/helpers/ActionsCreator.java:
--------------------------------------------------------------------------------
1 | package redux.api.helpers;
2 |
3 | public class ActionsCreator {
4 | public static Object unknownAction() {
5 | return "unknownAction";
6 | }
7 |
8 | public static Object addTodo(String message) {
9 | return new Actions.AddTodo(message);
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/specs/src/main/java/redux/api/helpers/Reducers.java:
--------------------------------------------------------------------------------
1 | package redux.api.helpers;
2 |
3 | import redux.api.Reducer;
4 |
5 | import java.util.ArrayList;
6 |
7 | public class Reducers {
8 | private static int nexId(State state) {
9 | int maxId = 0;
10 | for (Todo todo : state.todos) {
11 | maxId = Math.max(todo.id, maxId);
12 | }
13 | return maxId + 1;
14 | }
15 |
16 | public static final Reducer TODOS = new Reducer() {
17 | @Override
18 | public State reduce(State state, Object action) {
19 | if (action instanceof Actions.AddTodo) {
20 | final String message = ((Actions.AddTodo) action).message;
21 | final ArrayList newTodos = new ArrayList<>();
22 | newTodos.addAll(state.todos);
23 | newTodos.add(new Todo(nexId(state), message));
24 | return new State(newTodos);
25 | } else {
26 | return state;
27 | }
28 | }
29 | };
30 |
31 | public static final Reducer TODOS_REVERSE = new Reducer() {
32 | @Override
33 | public State reduce(State state, Object action) {
34 | if (action instanceof Actions.AddTodo) {
35 | final String message = ((Actions.AddTodo) action).message;
36 | final ArrayList newTodos = new ArrayList<>();
37 | newTodos.add(new Todo(nexId(state), message));
38 | newTodos.addAll(state.todos);
39 | return new State(newTodos);
40 | } else {
41 | return state;
42 | }
43 | }
44 | };
45 | }
46 |
--------------------------------------------------------------------------------
/specs/src/main/java/redux/api/helpers/State.java:
--------------------------------------------------------------------------------
1 | package redux.api.helpers;
2 |
3 | import java.util.Collections;
4 | import java.util.List;
5 |
6 | import static java.util.Arrays.asList;
7 | import static java.util.Collections.unmodifiableList;
8 |
9 | public final class State {
10 | final List todos;
11 |
12 | public State(Todo... todos) {
13 | this(asList(todos));
14 | }
15 |
16 | public State(List todos) {
17 | this.todos = unmodifiableList(todos);
18 | }
19 |
20 | public State() {
21 | this(Collections.emptyList());
22 | }
23 |
24 | @Override
25 | public boolean equals(Object o) {
26 | if (this == o) {
27 | return true;
28 | }
29 | if (o == null || getClass() != o.getClass()) {
30 | return false;
31 | }
32 |
33 | final State state = (State) o;
34 | return todos != null ? todos.equals(state.todos) : state.todos == null;
35 |
36 | }
37 |
38 | @Override
39 | public int hashCode() {
40 | return todos != null ? todos.hashCode() : 0;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/specs/src/main/java/redux/api/helpers/Todo.java:
--------------------------------------------------------------------------------
1 | package redux.api.helpers;
2 |
3 | public final class Todo {
4 | final int id;
5 | final String message;
6 |
7 | public Todo(int id, String message) {
8 | this.id = id;
9 | this.message = message;
10 | }
11 |
12 | @Override
13 | public boolean equals(Object o) {
14 | if (this == o) {
15 | return true;
16 | }
17 | if (o == null || getClass() != o.getClass()) {
18 | return false;
19 | }
20 |
21 | final Todo todo = (Todo) o;
22 |
23 | if (id != todo.id) return false;
24 | return message != null ? message.equals(todo.message) : todo.message == null;
25 | }
26 |
27 | @Override
28 | public int hashCode() {
29 | int result = id;
30 | result = 31 * result + (message != null ? message.hashCode() : 0);
31 | return result;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------