├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── docs └── resources │ ├── gopher.png │ └── myMachine.png ├── errors.go ├── examples ├── drinkMachine │ ├── app │ │ └── main.go │ └── drinkMachine.go └── simpleMachine │ ├── app │ └── main.go │ └── simpleMachine.go ├── go.mod ├── go.sum ├── state.go ├── stateMachine.go ├── stateMachine_test.go ├── state_test.go ├── stateful.go ├── statefulGraph └── stateMachineGraph.go ├── transition.go ├── transitionRule.go ├── transitionRule_test.go └── transition_test.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Vendor directory 15 | vendor 16 | .idea 17 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | 3 | go: 4 | - 1.x 5 | - 1.12.x 6 | 7 | env: 8 | - GO111MODULE=on 9 | 10 | install: true 11 | 12 | script: 13 | - go test -v -vet=all ./... -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Michael Bykovski 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 |

Welcome to stateful 👋

2 |

3 | 4 | Travis CI 5 | 6 | 7 | License: MIT 8 | 9 |

10 |

11 | 12 | Twitter: michaelbykovski 13 | 14 |

15 | 16 |

17 | 18 |

19 | 20 | 21 | > Create easy state machines with your existing code 22 | 23 | # Table of Contents 24 | 1. [Documentation](#documentation) 25 | 2. [Usage](#usage) 26 | 3. [Draw graph](#draw-graph) 27 | 4. [Wildcards](#wildcards) 28 | 5. [Examples](#examples) 29 | 6. [Credits](#credits) 30 | 31 | ## Documentation 32 | 33 | You can find the documentation here: [https://pkg.go.dev/github.com/bykof/stateful?tab=doc](https://pkg.go.dev/github.com/bykof/stateful?tab=doc) 34 | 35 | ## Usage 36 | 37 | It is very easy to use stateful. 38 | Just create a struct and implement the `stateful` interface 39 | ```go 40 | import "github.com/bykof/stateful" 41 | 42 | type ( 43 | MyMachine struct { 44 | state stateful.State 45 | amount int 46 | } 47 | 48 | AmountParams struct { 49 | Amount int 50 | } 51 | ) 52 | 53 | func NewMyMachine() MyMachine { 54 | return MyMachine{ 55 | state: A, 56 | amount: 0, 57 | } 58 | } 59 | 60 | // State implement interface stateful 61 | func (mm MyMachine) State() stateful.State { 62 | return mm.state 63 | } 64 | 65 | // SetState implement interface stateful 66 | func (mm *MyMachine) SetState(state stateful.State) error { 67 | mm.state = state 68 | return nil 69 | } 70 | ``` 71 | 72 | Declare some proper states: 73 | ```go 74 | const ( 75 | A = stateful.DefaultState("A") 76 | B = stateful.DefaultState("B") 77 | ) 78 | ``` 79 | 80 | Then add some transitions to the machine: 81 | ```go 82 | // Declare a transition of you machine and return the new state of the machine. 83 | func (mm *MyMachine) FromAToB(transitionArguments stateful.TransitionArguments) (stateful.State, error) { 84 | amountParams, ok := transitionArguments.(AmountParams) 85 | if !ok { 86 | return nil, errors.New("could not parse AmountParams") 87 | } 88 | 89 | mm.amount += amountParams.Amount 90 | return B, nil 91 | } 92 | 93 | func (mm *MyMachine) FromBToA(transitionArguments stateful.TransitionArguments) (stateful.State, error) { 94 | amountParams, ok := transitionArguments.(AmountParams) 95 | if !ok { 96 | return nil, errors.New("could not parse AmountParams") 97 | } 98 | 99 | mm.amount -= amountParams.Amount 100 | return A, nil 101 | } 102 | 103 | // The state machine will check, if you transfer to a proper and defined state in the machine. See below. 104 | func (mm *MyMachine) FromAToNotExistingC(_ stateful.TransitionArguments) (stateful.State, error) { 105 | return stateful.DefaultState("C") 106 | } 107 | ``` 108 | 109 | And now initialize the machine: 110 | ```go 111 | myMachine := NewMyMachine() 112 | stateMachine := &stateful.StateMachine{ 113 | StatefulObject: &myMachine, 114 | } 115 | 116 | stateMachine.AddTransition( 117 | // The transition function 118 | myMachine.FromAToB, 119 | // SourceStates 120 | stateful.States{A}, 121 | // DestinationStates 122 | stateful.States{B}, 123 | ) 124 | 125 | stateMachine.AddTransition( 126 | myMachine.FromBToA, 127 | stateful.States{B}, 128 | stateful.States{A}, 129 | ) 130 | ``` 131 | 132 | Everything is done! Now run the machine: 133 | ```go 134 | _ := stateMachine.Run( 135 | // The transition function 136 | myMachine.FromAToB, 137 | // The transition params which will be passed to the transition function 138 | stateful.TransitionArguments(AmountParams{Amount: 1}), 139 | ) 140 | 141 | _ = stateMachine.Run( 142 | myMachine.FromBToA, 143 | stateful.TransitionArguments(AmountParams{Amount: 1}), 144 | ) 145 | 146 | err := stateMachine.Run( 147 | myMachine.FromBToA, 148 | stateful.TransitionArguments(AmountParams{Amount: 1}), 149 | ) 150 | 151 | // We cannot run the transition "FromBToA" from current state "A"... 152 | if err != nil { 153 | log.Fatal(err) // will print: you cannot run FromAToB from state A 154 | } 155 | 156 | // We cannot transfer the machine with current transition to returned state "C" 157 | err = stateMachine.Run( 158 | myMachine.FromAToNotExistingC, 159 | stateful.TransitionArguments(nil), 160 | ) 161 | 162 | if err != nil { 163 | log.Fatal(err) // will print: you cannot transfer to state C 164 | } 165 | ``` 166 | 167 | That's it! 168 | 169 | ## Draw graph 170 | 171 | You can draw a graph of your state machine in `dot` format of graphviz. 172 | 173 | Just pass in your created statemachine into the StateMachineGraph. 174 | 175 | ```go 176 | import "github.com/bykof/stateful/src/statefulGraph" 177 | stateMachineGraph := statefulGraph.StateMachineGraph{StateMachine: *stateMachine} 178 | _ = stateMachineGraph.DrawGraph() 179 | ``` 180 | 181 | This will print following to the console: 182 | ``` 183 | digraph { 184 | A->B[ label="FromAToB" ]; 185 | B->A[ label="FromBToA" ]; 186 | A; 187 | B; 188 | 189 | } 190 | ``` 191 | 192 | which is actually this graph: 193 | 194 | ![MyMachine Transition Graph](https://github.com/bykof/stateful/raw/master/docs/resources/myMachine.png) 195 | 196 | ## Wildcards 197 | 198 | You can also address wildcards as SourceStates or DestinationStates 199 | 200 | ``` 201 | stateMachine.AddTransition( 202 | myMachine.FromBToAllStates, 203 | stateful.States{B}, 204 | stateful.States{stateful.AllStates}, 205 | ) 206 | ``` 207 | 208 | This will give you the opportunity to jump e.g. B to AllStates. 209 | 210 | *Keep in mind that `AllStates` creates a lot of complexity and maybe a missbehavior. 211 | So use it only if you are knowing what you are doing* 212 | 213 | ## Examples 214 | 215 | Have a look at the examples: [examples](https://github.com/bykof/stateful/tree/master/examples) 216 | 217 | 218 | ## Credits 219 | 220 | Thank you [calhoun](https://www.calhoun.io/) for the sweet gopher image! 221 | 222 | ## Run tests 223 | 224 | ```sh 225 | go test ./... 226 | ``` 227 | 228 | ## Author 229 | 230 | 👤 **Michael Bykovski** 231 | 232 | * Twitter: [@michaelbykovski](https://twitter.com/michaelbykovski) 233 | * Github: [@bykof](https://github.com/bykof) 234 | 235 | ## Show your support 236 | 237 | Give a ⭐️ if this project helped you! 238 | 239 | ## 📝 License 240 | 241 | Copyright © 2019 [Michael Bykovski](https://github.com/bykof).
242 | This project is [MIT](https://opensource.org/licenses/MIT) licensed. -------------------------------------------------------------------------------- /docs/resources/gopher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/stateful/163d20827904d3193d9996cfc9b3a4f1bac6251b/docs/resources/gopher.png -------------------------------------------------------------------------------- /docs/resources/myMachine.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bykof/stateful/163d20827904d3193d9996cfc9b3a4f1bac6251b/docs/resources/myMachine.png -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type ( 8 | TransitionRuleNotFoundError struct { 9 | transition Transition 10 | } 11 | 12 | CannotRunFromStateError struct { 13 | stateMachine StateMachine 14 | transitionRule TransitionRule 15 | } 16 | 17 | CannotTransferToStateError struct { 18 | state State 19 | } 20 | ) 21 | 22 | func NewTransitionRuleNotFoundError(transition Transition) *TransitionRuleNotFoundError { 23 | return &TransitionRuleNotFoundError{ 24 | transition: transition, 25 | } 26 | } 27 | 28 | func NewCannotRunFromStateError(stateMachine StateMachine, transitionRule TransitionRule) *CannotRunFromStateError { 29 | return &CannotRunFromStateError{ 30 | stateMachine: stateMachine, 31 | transitionRule: transitionRule, 32 | } 33 | } 34 | 35 | func NewCannotTransferToStateError(state State) *CannotTransferToStateError { 36 | return &CannotTransferToStateError{ 37 | state: state, 38 | } 39 | } 40 | 41 | func (trnfe TransitionRuleNotFoundError) Error() string { 42 | return fmt.Sprintf( 43 | "no transitionRule found for given transition %s", 44 | trnfe.transition.GetName(), 45 | ) 46 | } 47 | 48 | func (crfse CannotRunFromStateError) Error() string { 49 | return fmt.Sprintf( 50 | "you cannot run %s from state %s", 51 | crfse.transitionRule.Transition.GetName(), 52 | crfse.stateMachine.StatefulObject.State(), 53 | ) 54 | } 55 | 56 | func (cttse CannotTransferToStateError) Error() string { 57 | return fmt.Sprintf( 58 | "you cannot transfer to state %s", 59 | cttse.state, 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /examples/drinkMachine/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bykof/stateful" 5 | "github.com/bykof/stateful/examples/drinkMachine" 6 | "github.com/bykof/stateful/statefulGraph" 7 | ) 8 | 9 | func main() { 10 | drinkMachineObject := drinkMachine.NewMachine() 11 | stateMachine := &stateful.StateMachine{ 12 | StatefulObject: &drinkMachineObject, 13 | } 14 | stateMachine.AddTransition( 15 | drinkMachineObject.InsertCoin, 16 | stateful.States{ 17 | drinkMachine.Ready, 18 | }, 19 | stateful.States{ 20 | drinkMachine.CollectedEnoughMoney, 21 | }, 22 | ) 23 | 24 | stateMachine.AddTransition( 25 | drinkMachineObject.Cancel, 26 | stateful.States{ 27 | stateful.AllStates, 28 | }, 29 | stateful.States{ 30 | drinkMachine.Cancelled, 31 | }, 32 | ) 33 | 34 | stateMachine.AddTransition( 35 | drinkMachineObject.DropDrink, 36 | stateful.States{ 37 | drinkMachine.CollectedEnoughMoney, 38 | }, 39 | stateful.States{ 40 | drinkMachine.DroppedDrink, 41 | }, 42 | ) 43 | 44 | stateMachine.AddTransition( 45 | drinkMachineObject.DropChange, 46 | stateful.States{ 47 | drinkMachine.Cancelled, 48 | drinkMachine.DroppedDrink, 49 | }, 50 | stateful.States{ 51 | drinkMachine.Ready, 52 | }, 53 | ) 54 | 55 | stateMachine.AddTransition( 56 | drinkMachineObject.GoToMaintenance, 57 | stateful.States{ 58 | drinkMachine.Ready, 59 | }, 60 | stateful.States{ 61 | drinkMachine.Maintenance, 62 | }, 63 | ) 64 | 65 | stateMachine.AddTransition( 66 | drinkMachineObject.GoToAny, 67 | stateful.States{ 68 | drinkMachine.Maintenance, 69 | }, 70 | stateful.States{ 71 | stateful.AllStates, 72 | }, 73 | ) 74 | stateMachineGraph := statefulGraph.StateMachineGraph{StateMachine: *stateMachine} 75 | _ = stateMachineGraph.DrawGraph() 76 | } 77 | -------------------------------------------------------------------------------- /examples/drinkMachine/drinkMachine.go: -------------------------------------------------------------------------------- 1 | package drinkMachine 2 | 3 | import ( 4 | "github.com/bykof/stateful" 5 | "github.com/pkg/errors" 6 | ) 7 | 8 | const ( 9 | PricePerDrink = 10 10 | Ready = stateful.DefaultState("Ready") 11 | CollectedEnoughMoney = stateful.DefaultState("CollectedEnoughMoney") 12 | DroppedDrink = stateful.DefaultState("DroppedDrink") 13 | Cancelled = stateful.DefaultState("Cancelled") 14 | Maintenance = stateful.DefaultState("Maintenance") 15 | ) 16 | 17 | type ( 18 | DrinkMachine struct { 19 | state stateful.State 20 | currentAmount int 21 | collectedCoins int 22 | } 23 | 24 | InsertCoinParam struct { 25 | Amount int 26 | } 27 | ) 28 | 29 | func (cp *InsertCoinParam) GetData() interface{} { 30 | return cp 31 | } 32 | 33 | func NewMachine() DrinkMachine { 34 | return DrinkMachine{state: stateful.DefaultState(Ready)} 35 | } 36 | 37 | func (m DrinkMachine) State() stateful.State { 38 | return m.state 39 | } 40 | 41 | func (m *DrinkMachine) SetState(state stateful.State) error { 42 | m.state = state 43 | return nil 44 | } 45 | 46 | func (m DrinkMachine) GetCurrentAmount() int { 47 | return m.currentAmount 48 | } 49 | 50 | func (m DrinkMachine) GetCollectedCoins() int { 51 | return m.collectedCoins 52 | } 53 | 54 | func (m *DrinkMachine) InsertCoin(transitionArguments stateful.TransitionArguments) (stateful.State, error) { 55 | coinParams, ok := transitionArguments.(InsertCoinParam) 56 | if !ok { 57 | return nil, errors.New("cannot parse coinparams") 58 | } 59 | 60 | m.currentAmount += coinParams.Amount 61 | if m.currentAmount >= PricePerDrink { 62 | return CollectedEnoughMoney, nil 63 | } 64 | return m.state, nil 65 | } 66 | 67 | func (m *DrinkMachine) Cancel(_ stateful.TransitionArguments) (stateful.State, error) { 68 | return Cancelled, nil 69 | } 70 | 71 | func (m *DrinkMachine) GoToMaintenance(_ stateful.TransitionArguments) (stateful.State, error) { 72 | return Maintenance, nil 73 | } 74 | 75 | func (m *DrinkMachine) GoToAny(_ stateful.TransitionArguments) (stateful.State, error) { 76 | return Ready, nil 77 | } 78 | 79 | func (m *DrinkMachine) DropChange(_ stateful.TransitionArguments) (stateful.State, error) { 80 | m.currentAmount = 0 81 | return Ready, nil 82 | } 83 | 84 | func (m *DrinkMachine) DropDrink(_ stateful.TransitionArguments) (stateful.State, error) { 85 | m.collectedCoins += PricePerDrink 86 | m.currentAmount -= PricePerDrink 87 | return DroppedDrink, nil 88 | } 89 | 90 | func (m *DrinkMachine) NotAvailable(_ stateful.TransitionArguments) (stateful.State, error) { 91 | return DroppedDrink, nil 92 | } 93 | -------------------------------------------------------------------------------- /examples/simpleMachine/app/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/bykof/stateful" 5 | "github.com/bykof/stateful/examples/simpleMachine" 6 | "github.com/bykof/stateful/statefulGraph" 7 | ) 8 | 9 | func main() { 10 | myMachine := simpleMachine.NewMyMachine() 11 | stateMachine := &stateful.StateMachine{ 12 | StatefulObject: &myMachine, 13 | } 14 | 15 | stateMachine.AddTransition( 16 | myMachine.FromAToB, 17 | stateful.States{simpleMachine.A}, 18 | stateful.States{simpleMachine.B}, 19 | ) 20 | 21 | stateMachine.AddTransition( 22 | myMachine.FromBToA, 23 | stateful.States{simpleMachine.B}, 24 | stateful.States{simpleMachine.A}, 25 | ) 26 | 27 | _ = stateMachine.Run( 28 | myMachine.FromAToB, 29 | simpleMachine.AmountArguments{Amount: 1}, 30 | ) 31 | 32 | _ = stateMachine.Run( 33 | myMachine.FromBToA, 34 | simpleMachine.AmountArguments{Amount: 1}, 35 | ) 36 | 37 | stateMachineGraph := statefulGraph.StateMachineGraph{StateMachine: *stateMachine} 38 | _ = stateMachineGraph.DrawGraph() 39 | } 40 | -------------------------------------------------------------------------------- /examples/simpleMachine/simpleMachine.go: -------------------------------------------------------------------------------- 1 | package simpleMachine 2 | 3 | import ( 4 | "errors" 5 | "github.com/bykof/stateful" 6 | ) 7 | 8 | var ( 9 | A = stateful.DefaultState("A") 10 | B = stateful.DefaultState("B") 11 | ) 12 | 13 | type ( 14 | MyMachine struct { 15 | state stateful.State 16 | amount int 17 | } 18 | 19 | AmountArguments struct { 20 | Amount int 21 | } 22 | ) 23 | 24 | func NewMyMachine() MyMachine { 25 | return MyMachine{ 26 | state: A, 27 | amount: 0, 28 | } 29 | } 30 | 31 | func (mm MyMachine) State() stateful.State { 32 | return mm.state 33 | } 34 | 35 | func (mm *MyMachine) SetState(state stateful.State) error { 36 | mm.state = state 37 | return nil 38 | } 39 | 40 | func (mm *MyMachine) FromAToB(transitionArguments stateful.TransitionArguments) (stateful.State, error) { 41 | amountArguments, ok := transitionArguments.(AmountArguments) 42 | if !ok { 43 | return nil, errors.New("could not parse transitionarguments as amountarguments") 44 | } 45 | mm.amount += amountArguments.Amount 46 | return B, nil 47 | } 48 | 49 | func (mm *MyMachine) FromBToA(transitionArguments stateful.TransitionArguments) (stateful.State, error) { 50 | amountArguments, ok := transitionArguments.(AmountArguments) 51 | if !ok { 52 | return nil, errors.New("could not parse transitionarguments as amountarguments") 53 | } 54 | mm.amount -= amountArguments.Amount 55 | return A, nil 56 | } 57 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/bykof/stateful 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/awalterschulze/gographviz v0.0.0-20190522210029-fa59802746ab 7 | github.com/pkg/errors v0.8.1 8 | github.com/stretchr/testify v1.3.0 9 | ) 10 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/awalterschulze/gographviz v0.0.0-20190522210029-fa59802746ab h1:+cdNqtOJWjvepyhxy23G7z7vmpYCoC65AP0nqi1f53s= 2 | github.com/awalterschulze/gographviz v0.0.0-20190522210029-fa59802746ab/go.mod h1:GEV5wmg4YquNw7v1kkyoX9etIk8yVmXj+AkDHuuETHs= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 6 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= 11 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 12 | -------------------------------------------------------------------------------- /state.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | const ( 4 | // AllStates is a wildcard which represents all states in the state machine 5 | AllStates = DefaultState("*") 6 | ) 7 | 8 | type ( 9 | // State represents a state of a stateful object 10 | State interface { 11 | GetID() string 12 | IsWildCard() bool 13 | } 14 | 15 | // States are a slice of State 16 | States []State 17 | 18 | // DefaultState is a string which should be used in every stateful object as the state 19 | DefaultState string 20 | ) 21 | 22 | // IsWildCard checks if the current state is a wildcard. 23 | // So if the state stands for all possible states 24 | func (ds DefaultState) IsWildCard() bool { 25 | return ds == AllStates 26 | } 27 | 28 | // GetID returns the string representation of the DefaultState 29 | func (ds DefaultState) GetID() string { 30 | return string(ds) 31 | } 32 | 33 | // Contains search in States if the given state is inside the States. 34 | // It compares with GetID 35 | func (ss States) Contains(state State) bool { 36 | for _, currentState := range ss { 37 | if currentState.GetID() == state.GetID() { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // HasWildCard checks if there is a wildcard state inside of States 45 | func (ss States) HasWildCard() bool { 46 | for _, currentState := range ss { 47 | if currentState.IsWildCard() { 48 | return true 49 | } 50 | } 51 | return false 52 | } 53 | -------------------------------------------------------------------------------- /stateMachine.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | type ( 4 | // StateMachine handles the state of the StatefulObject 5 | StateMachine struct { 6 | StatefulObject Stateful 7 | transitionRules TransitionRules 8 | } 9 | ) 10 | 11 | // AddTransition adds a transition to the state machine. 12 | func (sm *StateMachine) AddTransition( 13 | transition Transition, 14 | sourceStates States, 15 | destinationStates States, 16 | ) { 17 | sm.transitionRules = append( 18 | sm.transitionRules, 19 | TransitionRule{ 20 | SourceStates: sourceStates, 21 | Transition: transition, 22 | DestinationStates: destinationStates, 23 | }, 24 | ) 25 | } 26 | 27 | // GetTransitionRules returns all transitionRules in the state machine 28 | func (sm StateMachine) GetTransitionRules() TransitionRules { 29 | return sm.transitionRules 30 | } 31 | 32 | // GetAllStates returns all known and possible states by the state machine 33 | func (sm StateMachine) GetAllStates() States { 34 | states := States{} 35 | keys := make(map[State]bool) 36 | 37 | for _, transitionRule := range sm.transitionRules { 38 | for _, state := range append(transitionRule.SourceStates, transitionRule.DestinationStates...) { 39 | if _, ok := keys[state]; !ok { 40 | keys[state] = true 41 | if !state.IsWildCard() { 42 | states = append(states, state) 43 | } 44 | } 45 | } 46 | } 47 | return states 48 | } 49 | 50 | // Run runs the state machine with the given transition. 51 | // If the transition 52 | func (sm StateMachine) Run( 53 | transition Transition, 54 | transitionArguments TransitionArguments, 55 | ) error { 56 | transitionRule := sm.transitionRules.Find(transition) 57 | if transitionRule == nil { 58 | return NewTransitionRuleNotFoundError(transition) 59 | } 60 | 61 | if !transitionRule.IsAllowedToRun(sm.StatefulObject.State()) { 62 | return NewCannotRunFromStateError(sm, *transitionRule) 63 | } 64 | 65 | newState, err := transition(transitionArguments) 66 | if err != nil { 67 | return err 68 | } 69 | 70 | if !transitionRule.IsAllowedToTransfer(newState) { 71 | return NewCannotTransferToStateError(newState) 72 | } 73 | 74 | err = sm.StatefulObject.SetState(newState) 75 | if err != nil { 76 | return err 77 | } 78 | return nil 79 | } 80 | 81 | func (sm StateMachine) GetAvailableTransitions() Transitions { 82 | transitions := Transitions{} 83 | for _, transitionRule := range sm.transitionRules { 84 | if transitionRule.IsAllowedToRun(sm.StatefulObject.State()) { 85 | transitions = append(transitions, transitionRule.Transition) 86 | } 87 | } 88 | return transitions 89 | } 90 | -------------------------------------------------------------------------------- /stateMachine_test.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | import ( 4 | "errors" 5 | "github.com/stretchr/testify/assert" 6 | "reflect" 7 | "testing" 8 | ) 9 | 10 | const ( 11 | State1 = DefaultState("State1") 12 | State2 = DefaultState("State2") 13 | State3 = DefaultState("State3") 14 | State4 = DefaultState("State4") 15 | ) 16 | 17 | type ( 18 | TestStatefulObject struct { 19 | state State 20 | TestValue int 21 | } 22 | 23 | TestParam struct { 24 | Amount int 25 | } 26 | ) 27 | 28 | func (tsp TestStatefulObject) State() State { 29 | return tsp.state 30 | } 31 | 32 | func (tsp *TestStatefulObject) SetState(state State) error { 33 | tsp.state = state 34 | return nil 35 | } 36 | 37 | func (tsp *TestStatefulObject) FromState1ToState2(_ TransitionArguments) (State, error) { 38 | return State2, nil 39 | } 40 | 41 | func (tsp *TestStatefulObject) FromState2ToState3(params TransitionArguments) (State, error) { 42 | testParam, _ := params.(TestParam) 43 | tsp.TestValue += testParam.Amount 44 | return State3, nil 45 | } 46 | 47 | func (tsp *TestStatefulObject) FromState3ToState1And2(_ TransitionArguments) (State, error) { 48 | return State1, nil 49 | } 50 | 51 | func (tsp *TestStatefulObject) FromState2And3To4(_ TransitionArguments) (State, error) { 52 | return State4, nil 53 | } 54 | 55 | func (tsp *TestStatefulObject) FromState4ToState1(_ TransitionArguments) (State, error) { 56 | return State1, nil 57 | } 58 | 59 | func (tsp *TestStatefulObject) ErrorBehavior(_ TransitionArguments) (State, error) { 60 | return nil, errors.New("there was an error") 61 | } 62 | 63 | func (tsp TestStatefulObject) NotExistingTransition(_ TransitionArguments) (State, error) { 64 | return nil, nil 65 | } 66 | 67 | func (tsp TestStatefulObject) FromState3ToNotExistingState(_ TransitionArguments) (State, error) { 68 | return DefaultState("NotExisting"), nil 69 | } 70 | 71 | func NewTestStatefulObject() TestStatefulObject { 72 | return TestStatefulObject{state: State1} 73 | } 74 | 75 | func NewStateMachine() StateMachine { 76 | testStatefulObject := NewTestStatefulObject() 77 | stateMachine := StateMachine{StatefulObject: &testStatefulObject} 78 | stateMachine.AddTransition(testStatefulObject.FromState1ToState2, States{State1}, States{State2}) 79 | stateMachine.AddTransition(testStatefulObject.FromState2ToState3, States{State2}, States{State3}) 80 | stateMachine.AddTransition(testStatefulObject.FromState3ToState1And2, States{State3}, States{State1, State2}) 81 | stateMachine.AddTransition(testStatefulObject.FromState2And3To4, States{State2, State3}, States{State4}) 82 | stateMachine.AddTransition(testStatefulObject.FromState4ToState1, States{State4}, States{State1}) 83 | stateMachine.AddTransition(testStatefulObject.ErrorBehavior, States{AllStates}, States{State1, State2}) 84 | stateMachine.AddTransition(testStatefulObject.FromState3ToNotExistingState, States{State3}, States{}) 85 | return stateMachine 86 | } 87 | 88 | func TestStateMachine_AddTransition(t *testing.T) { 89 | testStatefulObject := NewTestStatefulObject() 90 | stateMachine := StateMachine{StatefulObject: &testStatefulObject} 91 | stateMachine.AddTransition(testStatefulObject.FromState1ToState2, States{State1}, States{State2}) 92 | stateMachine.AddTransition(testStatefulObject.ErrorBehavior, States{State2, State4}, States{State2, State3}) 93 | 94 | assert.ElementsMatch( 95 | t, 96 | States{State1}, 97 | stateMachine.transitionRules[0].SourceStates, 98 | ) 99 | assert.ElementsMatch( 100 | t, 101 | States{State2}, 102 | stateMachine.transitionRules[0].DestinationStates, 103 | ) 104 | 105 | transition := Transition(testStatefulObject.FromState1ToState2) 106 | assert.Equal( 107 | t, 108 | transition.GetName(), 109 | stateMachine.transitionRules[0].Transition.GetName(), 110 | ) 111 | 112 | assert.ElementsMatch( 113 | t, 114 | States{State2, State4}, 115 | stateMachine.transitionRules[1].SourceStates, 116 | ) 117 | assert.ElementsMatch( 118 | t, 119 | States{State2, State3}, 120 | stateMachine.transitionRules[1].DestinationStates, 121 | ) 122 | 123 | transition = Transition(testStatefulObject.ErrorBehavior) 124 | assert.Equal( 125 | t, 126 | transition.GetName(), 127 | stateMachine.transitionRules[1].Transition.GetName(), 128 | ) 129 | } 130 | 131 | func TestStateMachine_GetAllStates(t *testing.T) { 132 | testStatefulObject := NewTestStatefulObject() 133 | stateMachine := StateMachine{StatefulObject: &testStatefulObject} 134 | stateMachine.AddTransition(testStatefulObject.FromState1ToState2, States{State1}, States{State2}) 135 | stateMachine.AddTransition(testStatefulObject.ErrorBehavior, States{State2, State4}, States{State2, State3}) 136 | 137 | assert.ElementsMatch( 138 | t, 139 | States{ 140 | State1, 141 | State2, 142 | State3, 143 | State4, 144 | }, 145 | stateMachine.GetAllStates(), 146 | ) 147 | } 148 | 149 | func TestStateMachine_Run(t *testing.T) { 150 | stateMachine := NewStateMachine() 151 | testStatefulObject := stateMachine.StatefulObject.(*TestStatefulObject) 152 | err := stateMachine.Run( 153 | testStatefulObject.FromState1ToState2, 154 | TransitionArguments(nil), 155 | ) 156 | assert.NoError(t, err) 157 | assert.Equal(t, State2, testStatefulObject.State()) 158 | 159 | err = stateMachine.Run( 160 | testStatefulObject.FromState2ToState3, 161 | TransitionArguments(TestParam{Amount: 2}), 162 | ) 163 | 164 | assert.NoError(t, err) 165 | assert.Equal(t, State3, testStatefulObject.State()) 166 | assert.Equal(t, 2, testStatefulObject.TestValue) 167 | 168 | err = stateMachine.Run( 169 | testStatefulObject.FromState4ToState1, 170 | TransitionArguments(nil), 171 | ) 172 | assert.Error(t, err) 173 | assert.Equal( 174 | t, 175 | reflect.TypeOf(&CannotRunFromStateError{}), 176 | reflect.TypeOf(err), 177 | ) 178 | 179 | err = stateMachine.Run( 180 | testStatefulObject.ErrorBehavior, 181 | TransitionArguments(nil), 182 | ) 183 | assert.Error(t, err) 184 | assert.Equal(t, errors.New("there was an error"), err) 185 | 186 | err = stateMachine.Run( 187 | testStatefulObject.NotExistingTransition, 188 | nil, 189 | ) 190 | 191 | assert.Error(t, err) 192 | assert.Equal( 193 | t, 194 | reflect.TypeOf(&TransitionRuleNotFoundError{}), 195 | reflect.TypeOf(err), 196 | ) 197 | 198 | err = stateMachine.Run( 199 | testStatefulObject.FromState3ToNotExistingState, 200 | nil, 201 | ) 202 | 203 | assert.Error(t, err) 204 | assert.Equal( 205 | t, 206 | reflect.TypeOf(&CannotTransferToStateError{}), 207 | reflect.TypeOf(err), 208 | ) 209 | 210 | assert.True(t, reflect.TypeOf(&CannotTransferToStateError{}) == reflect.TypeOf(err)) 211 | } 212 | 213 | func TestStateMachine_GetAvailableTransitions(t *testing.T) { 214 | stateMachine := NewStateMachine() 215 | availableTransitions := stateMachine.GetAvailableTransitions() 216 | assert.Equal( 217 | t, 218 | Transition(stateMachine.StatefulObject.(*TestStatefulObject).FromState1ToState2).GetName(), 219 | availableTransitions[0].GetName(), 220 | ) 221 | } 222 | -------------------------------------------------------------------------------- /state_test.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func TestStates_Contains(t *testing.T) { 9 | state1 := DefaultState("state1") 10 | state2 := DefaultState("state2") 11 | state3 := DefaultState("state3") 12 | states := States{ 13 | state1, 14 | state2, 15 | } 16 | 17 | assert.True(t, states.Contains(state1)) 18 | assert.True(t, states.Contains(state2)) 19 | assert.False(t, states.Contains(state3)) 20 | } 21 | -------------------------------------------------------------------------------- /stateful.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | type ( 4 | // Stateful is the core interface which should be implemented by all stateful structs. 5 | // If this interface is implemented by a struct it can be processed by the state machine 6 | Stateful interface { 7 | // State returns the current state of the stateful object 8 | State() State 9 | 10 | // SetState sets the state of the stateful object and returns an error if it fails 11 | SetState(state State) error 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /statefulGraph/stateMachineGraph.go: -------------------------------------------------------------------------------- 1 | package statefulGraph 2 | 3 | import ( 4 | "fmt" 5 | "github.com/awalterschulze/gographviz" 6 | "github.com/bykof/stateful" 7 | ) 8 | 9 | type StateMachineGraph struct { 10 | StateMachine stateful.StateMachine 11 | } 12 | 13 | func (smg StateMachineGraph) DrawStates(graph *gographviz.Graph) error { 14 | for _, state := range smg.StateMachine.GetAllStates() { 15 | err := graph.AddNode(state.GetID(), state.GetID(), map[string]string{}) 16 | if err != nil { 17 | return err 18 | } 19 | } 20 | return nil 21 | } 22 | 23 | func (smg StateMachineGraph) DrawEdges(graph *gographviz.Graph) error { 24 | allStates := smg.StateMachine.GetAllStates() 25 | for _, transitionRule := range smg.StateMachine.GetTransitionRules() { 26 | sourceStates := transitionRule.SourceStates 27 | if sourceStates.HasWildCard() { 28 | sourceStates = allStates 29 | } 30 | 31 | destinationStates := transitionRule.DestinationStates 32 | if destinationStates.HasWildCard() { 33 | destinationStates = allStates 34 | } 35 | 36 | for _, sourceState := range sourceStates { 37 | for _, destinationState := range destinationStates { 38 | err := graph.AddEdge( 39 | sourceState.GetID(), 40 | destinationState.GetID(), 41 | true, 42 | map[string]string{ 43 | "label": fmt.Sprint("\"", transitionRule.Transition.GetName(), "\""), 44 | }, 45 | ) 46 | 47 | if err != nil { 48 | return err 49 | } 50 | } 51 | } 52 | } 53 | return nil 54 | } 55 | 56 | func (smg StateMachineGraph) DrawGraph() error { 57 | var err error 58 | graph, err := intializeGraphWithDir() 59 | if err != nil { 60 | return err 61 | } 62 | 63 | err = smg.drawGraph(graph) 64 | if err != nil { 65 | return err 66 | } 67 | 68 | fmt.Println(graph.String()) 69 | 70 | return nil 71 | } 72 | 73 | func (smg StateMachineGraph) DrawGraphWithName(name string) error { 74 | var err error 75 | graph, err := intializeGraphWithDir() 76 | if err != nil { 77 | return err 78 | } 79 | 80 | err = graph.SetName(name) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | err = smg.drawGraph(graph) 86 | if err != nil { 87 | return err 88 | } 89 | 90 | fmt.Println(graph.String()) 91 | 92 | return nil 93 | } 94 | 95 | func intializeGraphWithDir() (*gographviz.Graph, error) { 96 | var err error 97 | graph := gographviz.NewGraph() 98 | 99 | err = graph.SetDir(true) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return graph, nil 104 | } 105 | 106 | func (smg StateMachineGraph) drawGraph(graph *gographviz.Graph) error { 107 | err := smg.DrawStates(graph) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | err = smg.DrawEdges(graph) 113 | if err != nil { 114 | return err 115 | } 116 | return nil 117 | } 118 | -------------------------------------------------------------------------------- /transition.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | import ( 4 | "reflect" 5 | "runtime" 6 | "strings" 7 | ) 8 | 9 | type ( 10 | // TransitionArguments represents the arguments 11 | TransitionArguments interface{} 12 | 13 | // Transition represents the transition function which will be executed if the order is in the proper state 14 | // and there is a valid transitionRule in the state machine 15 | Transition func(transitionArguments TransitionArguments) (State, error) 16 | 17 | // Transitions are a slice of Transition 18 | Transitions []Transition 19 | ) 20 | 21 | func (t Transition) GetName() string { 22 | name := runtime.FuncForPC(reflect.ValueOf(t).Pointer()).Name() 23 | splittedName := strings.Split(name, ".") 24 | splittedActualName := strings.Split(splittedName[len(splittedName)-1], "-") 25 | return splittedActualName[0] 26 | } 27 | 28 | func (t Transition) GetID() uintptr { 29 | return reflect.ValueOf(t).Pointer() 30 | } 31 | 32 | func (ts Transitions) Contains(transition Transition) bool { 33 | for _, currentTransition := range ts { 34 | if currentTransition.GetID() == transition.GetID() { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | -------------------------------------------------------------------------------- /transitionRule.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | type ( 4 | TransitionRule struct { 5 | SourceStates States 6 | Transition Transition 7 | DestinationStates States 8 | } 9 | TransitionRules []TransitionRule 10 | ) 11 | 12 | func (tr TransitionRule) IsAllowedToRun(state State) bool { 13 | return tr.SourceStates.Contains(state) || tr.SourceStates.HasWildCard() 14 | } 15 | 16 | func (tr TransitionRule) IsAllowedToTransfer(state State) bool { 17 | return tr.DestinationStates.Contains(state) || tr.DestinationStates.HasWildCard() 18 | } 19 | 20 | func (trs TransitionRules) Find(transition Transition) *TransitionRule { 21 | for _, transitionRule := range trs { 22 | if transitionRule.Transition.GetID() == transition.GetID() { 23 | return &transitionRule 24 | } 25 | } 26 | return nil 27 | } -------------------------------------------------------------------------------- /transitionRule_test.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | func transitionruletestA(_ TransitionArguments) (State, error) { 9 | return nil, nil 10 | } 11 | func transitionruletestB(_ TransitionArguments) (State, error) { 12 | return nil, nil 13 | } 14 | func transitionruletestC(_ TransitionArguments) (State, error) { 15 | return nil, nil 16 | } 17 | 18 | func TestTransitionRule_IsAllowedToRun(t *testing.T) { 19 | transitionRule := TransitionRule{ 20 | SourceStates: States{ 21 | DefaultState("transitionTest_a"), 22 | DefaultState("transitionTest_b"), 23 | }, 24 | } 25 | assert.True(t, transitionRule.IsAllowedToRun(DefaultState("transitionTest_a"))) 26 | assert.True(t, transitionRule.IsAllowedToRun(DefaultState("transitionTest_b"))) 27 | assert.False(t, transitionRule.IsAllowedToRun(DefaultState("transitionTest_c"))) 28 | } 29 | 30 | func TestTransitionRule_IsAllowedToTransfer(t *testing.T) { 31 | transitionRule := TransitionRule{ 32 | DestinationStates: States{ 33 | DefaultState("transitionTest_a"), 34 | DefaultState("transitionTest_b"), 35 | }, 36 | } 37 | assert.True(t, transitionRule.IsAllowedToTransfer(DefaultState("transitionTest_a"))) 38 | assert.True(t, transitionRule.IsAllowedToTransfer(DefaultState("transitionTest_b"))) 39 | assert.False(t, transitionRule.IsAllowedToTransfer(DefaultState("transitionTest_c"))) 40 | } 41 | 42 | func TestTransitionRules_Find(t *testing.T) { 43 | transitionRules := TransitionRules{ 44 | TransitionRule{ 45 | Transition: transitionruletestA, 46 | }, 47 | TransitionRule{ 48 | Transition: transitionruletestB, 49 | }, 50 | } 51 | 52 | assert.Equal( 53 | t, 54 | TransitionRule{ 55 | Transition: transitionruletestA, 56 | }.Transition.GetID(), 57 | transitionRules.Find(transitionruletestA).Transition.GetID(), 58 | ) 59 | 60 | assert.Equal( 61 | t, 62 | TransitionRule{ 63 | Transition: transitionruletestB, 64 | }.Transition.GetID(), 65 | transitionRules.Find(transitionruletestB).Transition.GetID(), 66 | ) 67 | assert.Nil( 68 | t, 69 | transitionRules.Find(transitionruletestC), 70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /transition_test.go: -------------------------------------------------------------------------------- 1 | package stateful 2 | 3 | import ( 4 | "github.com/stretchr/testify/assert" 5 | "testing" 6 | ) 7 | 8 | type TransitionTestStruct struct{} 9 | 10 | func (tt TransitionTestStruct) abc(params TransitionArguments) (State, error) { 11 | return nil, nil 12 | } 13 | 14 | func transitiontestA(_ TransitionArguments) (State, error) { 15 | return nil, nil 16 | } 17 | func transitiontestB(_ TransitionArguments) (State, error) { 18 | return nil, nil 19 | } 20 | func transitiontestC(_ TransitionArguments) (State, error) { 21 | return nil, nil 22 | } 23 | 24 | func TestTransition_GetName(t *testing.T) { 25 | transitionTestStruct := TransitionTestStruct{} 26 | assert.Equal(t, "transitiontestA", Transition(transitiontestA).GetName()) 27 | assert.Equal(t, "transitiontestB", Transition(transitiontestB).GetName()) 28 | assert.Equal(t, "transitiontestC", Transition(transitiontestC).GetName()) 29 | assert.Equal(t, "abc", Transition(transitionTestStruct.abc).GetName()) 30 | } 31 | 32 | func TestTransitions_Contains(t *testing.T) { 33 | transitions := Transitions{ 34 | Transition(transitiontestA), 35 | Transition(transitiontestB), 36 | } 37 | 38 | assert.True(t, transitions.Contains(Transition(transitiontestA))) 39 | assert.True(t, transitions.Contains(Transition(transitiontestB))) 40 | assert.False(t, transitions.Contains(Transition(transitiontestC))) 41 | } 42 | --------------------------------------------------------------------------------