├── .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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
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 | 
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 |
--------------------------------------------------------------------------------