├── .travis.yml ├── LICENSE ├── README.md ├── gredux.go └── gredux_test.go /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | notifications: 4 | email: false 5 | 6 | os: 7 | - linux 8 | 9 | after_success: 10 | - bash <(curl -s https://codecov.io/bash) 11 | 12 | go: 13 | - 1.6 14 | 15 | script: go test -v -race -coverprofile=coverage.txt -covermode=atomic ./... 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Johnathan Howell 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gredux 2 | [![Go Report Card](https://goreportcard.com/badge/github.com/avahowell/gredux)](https://goreportcard.com/report/github.com/johnathanhowell/gredux) 3 | [![Build Status](https://travis-ci.org/avahowell/gredux.svg?branch=master)](https://travis-ci.org/johnathanhowell/gredux) 4 | [![codecov](https://codecov.io/gh/avahowell/gredux/branch/master/graph/badge.svg)](https://codecov.io/gh/johnathanhowell/gredux) 5 | [![GoDoc](https://godoc.org/github.com/avahowell/gredux?status.svg)](https://godoc.org/github.com/johnathanhowell/gredux) 6 | 7 | gredux is a golang implementation of a [redux](https://github.com/reactjs/redux)-esque state container. The aim is to provide a structure for writing applications which have consistent, predictable behaviour. 8 | 9 | ## Example Usage 10 | 11 | ```go 12 | 13 | import ( 14 | "github.com/avahowell/gredux" 15 | ) 16 | 17 | // Create an initial state for the Store 18 | type counterState struct { 19 | count int 20 | } 21 | 22 | // Instantiate a new store around this state 23 | store := gredux.New(counterState{0}) 24 | 25 | // Create a reducer which increments "count" when it receives an "increment" 26 | // action, and decrements when it receives a "decrement" action. 27 | store.Reducer(func(state gredux.State, action gredux.Action) gredux.State { 28 | switch action.ID { 29 | case "increment": 30 | return counterState{state.(counterState).count + action.Data.(int)} 31 | case "decrement": 32 | return counterState{state.(counterState).count - action.Data.(int)} 33 | default: 34 | return state 35 | } 36 | }) 37 | 38 | store.Dispatch(Action{"increment", 5}) 39 | store.Dispatch(Action{"decrement", 2}) 40 | 41 | fmt.Println(store.State().(counterState).count) // prints 3 42 | 43 | // Register a func to be called after each state update 44 | store.AfterUpdate(func(state State) { 45 | fmt.Println(state.(counterState).count) // prints the count after every state update 46 | }) 47 | store.Dispatch(Action{"decrement", 2}) 48 | ``` 49 | 50 | ## License 51 | The MIT License (MIT) 52 | -------------------------------------------------------------------------------- /gredux.go: -------------------------------------------------------------------------------- 1 | package gredux 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | type ( 8 | // State is the state of the gredux Store. 9 | State interface{} 10 | 11 | // Reducer is the func which receives actions dispatched 12 | // using Store.Dispatch() and updates the internal state. 13 | Reducer func(State, Action) State 14 | 15 | // Action defines a dispatchable data type that triggers updates in the Store. 16 | Action struct { 17 | ID string 18 | Data interface{} 19 | } 20 | 21 | // Store defines an immutable store of state. 22 | // The current state of the Store can be received by calling State() 23 | // but the state can only be changed by a Reducer as the result of a Dispatch'd Action. 24 | Store struct { 25 | mu sync.RWMutex 26 | reducer Reducer 27 | state State 28 | update func(State) 29 | } 30 | ) 31 | 32 | // New instantiates a new gredux Store. 33 | // initialState should be the struct used to define the Store's state. 34 | func New(initialState State) *Store { 35 | st := Store{ 36 | reducer: func(s State, a Action) State { 37 | return s 38 | }, 39 | state: initialState, 40 | } 41 | return &st 42 | } 43 | 44 | // Reducer sets the store's reducer function to the function `r`. 45 | func (st *Store) Reducer(r Reducer) { 46 | st.reducer = r 47 | } 48 | 49 | // AfterUpdate sets Store's update func. `update` is called after each 50 | // dispatch with a copy of the new state. 51 | func (st *Store) AfterUpdate(update func(State)) { 52 | st.update = update 53 | } 54 | 55 | // getState returns a copy of Store's current state map. 56 | func (st *Store) getState() State { 57 | return st.state 58 | } 59 | 60 | // State returns a copy of the current state. 61 | func (st *Store) State() State { 62 | st.mu.RLock() 63 | defer st.mu.RUnlock() 64 | return st.getState() 65 | } 66 | 67 | // Dispatch dispatches an Action into the Store. 68 | func (st *Store) Dispatch(action Action) { 69 | st.mu.Lock() 70 | defer st.mu.Unlock() 71 | st.state = st.reducer(st.getState(), action) 72 | if st.update != nil { 73 | st.update(st.getState()) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /gredux_test.go: -------------------------------------------------------------------------------- 1 | package gredux 2 | 3 | import ( 4 | "math/rand" 5 | "testing" 6 | "time" 7 | ) 8 | 9 | func TestDispatch(t *testing.T) { 10 | type testState struct { 11 | success bool 12 | } 13 | store := New(testState{false}) 14 | store.Dispatch(Action{"test", nil}) 15 | store.Reducer(func(state State, action Action) State { 16 | switch action.ID { 17 | case "test": 18 | return testState{true} 19 | default: 20 | return state 21 | } 22 | }) 23 | store.Dispatch(Action{"test", nil}) 24 | if st := store.State().(testState); !st.success { 25 | t.Fatal("expected reducer to set success") 26 | } 27 | } 28 | 29 | func TestDispatchUpdate(t *testing.T) { 30 | type testState struct { 31 | success bool 32 | } 33 | store := New(testState{false}) 34 | store.Reducer(func(state State, action Action) State { 35 | switch action.ID { 36 | case "test": 37 | return testState{true} 38 | default: 39 | return state 40 | } 41 | }) 42 | done := make(chan struct{}) 43 | store.AfterUpdate(func(state State) { 44 | defer close(done) 45 | if !state.(testState).success { 46 | t.Fatal() 47 | } 48 | }) 49 | store.Dispatch(Action{"test", nil}) 50 | select { 51 | case <-done: 52 | case <-time.After(time.Second): 53 | t.Fatal("OnUpdate func was not called after dispatch after 1 second") 54 | } 55 | } 56 | 57 | func TestDispatchIncrementDecrement(t *testing.T) { 58 | type counterState struct { 59 | count int 60 | } 61 | store := New(counterState{0}) 62 | store.Reducer(func(state State, action Action) State { 63 | switch action.ID { 64 | case "increment": 65 | return counterState{state.(counterState).count + action.Data.(int)} 66 | case "decrement": 67 | return counterState{state.(counterState).count - action.Data.(int)} 68 | default: 69 | return state 70 | } 71 | }) 72 | store.Dispatch(Action{"increment", 5}) 73 | if val := store.State().(counterState).count; val != 5 { 74 | t.Fatal("increment did not increment correctly") 75 | } 76 | store.Dispatch(Action{"increment", 3}) 77 | if val := store.State().(counterState).count; val != 8 { 78 | t.Fatal("increment did not increment correctly") 79 | } 80 | store.Dispatch(Action{"decrement", 2}) 81 | if val := store.State().(counterState).count; val != 6 { 82 | t.Fatal("decrement did not decrement correctly") 83 | } 84 | } 85 | 86 | func TestConcurrentDispatch(t *testing.T) { 87 | type testState struct { 88 | success bool 89 | } 90 | store := New(testState{false}) 91 | store.Reducer(func(state State, action Action) State { 92 | return testState{true} 93 | }) 94 | for i := 0; i < 10; i++ { 95 | go func() { 96 | time.Sleep(time.Second * time.Duration(rand.Int())) 97 | store.Dispatch(Action{"test", nil}) 98 | }() 99 | } 100 | } 101 | 102 | // TestStoreImmutability verifies that mutating the state passed 103 | // to AfterUpdate, Reducer, or returned by State() does not effect the internal state. 104 | func TestStoreImmutability(t *testing.T) { 105 | type testState struct { 106 | success bool 107 | mutated bool 108 | } 109 | store := New(testState{false, false}) 110 | store.Reducer(func(state State, action Action) State { 111 | st := state.(testState) 112 | if st.mutated { 113 | t.Fatal("state was mutated") 114 | } 115 | st.mutated = true 116 | switch action.ID { 117 | case "test": 118 | return testState{true, false} 119 | default: 120 | return state 121 | } 122 | }) 123 | i := 0 124 | done := make(chan struct{}) 125 | store.AfterUpdate(func(state State) { 126 | i++ 127 | if i == 2 { 128 | defer close(done) 129 | } 130 | st := state.(testState) 131 | if st.mutated { 132 | t.Fatal("state was mutated") 133 | } 134 | st.mutated = true 135 | }) 136 | store.Dispatch(Action{"test", nil}) 137 | store.Dispatch(Action{"test", nil}) 138 | st := store.State().(testState) 139 | st.mutated = true 140 | select { 141 | case <-done: 142 | // success! 143 | case <-time.After(time.Second): 144 | t.Fatal("TestStoreImmutability timed out after one second") 145 | } 146 | if store.State().(testState).mutated { 147 | t.Fatal("store was mutated") 148 | } 149 | } 150 | 151 | func BenchmarkDispatch(b *testing.B) { 152 | type counterState struct { 153 | count int 154 | } 155 | store := New(counterState{0}) 156 | store.Reducer(func(state State, action Action) State { 157 | switch action.ID { 158 | case "increment": 159 | return counterState{state.(counterState).count + 1} 160 | default: 161 | return state 162 | } 163 | }) 164 | 165 | for i := 0; i < b.N; i++ { 166 | store.Dispatch(Action{"increment", nil}) 167 | } 168 | } 169 | --------------------------------------------------------------------------------