1 /// An event machine structure slightly resembling finite automata.
2 module subscribed.event_machine;
3 
4 import std.traits : isCallable, EnumMembers;
5 
6 import subscribed.event;
7 
8 /**
9  * A state machine with simplified transitions - a wrapper around a state collection.
10  * State-dependent transitions should be implemented using beforeEach/afterEach hooks.
11  * A transition can be canceled by returning false from the any beforeEach listeners.
12  * The machine has an initial state and a set of alternative states (loops are permitted).
13  * Any state can transition to any other state (the initial state is unreachable after transitioning away from it).
14  *
15  * Params:
16  *  State_ = An enum of available states.
17  *  T = The listener type this event contains. Default is `void delegate()`.
18  */
19 struct EventMachine(State_, T = void delegate())
20     if (is(typeof(State_.init)) &&
21         State_.min != State_.max &&
22         isCallable!T)
23 {
24     /// An alias to the $(DDOC_PSYMBOL State_) parameter.
25     alias State = State_;
26 
27     /// The events' type.
28     alias EventType = Event!T;
29 
30     /// The hook to be executed before any transition. If false is returned, no transition occurs.
31     Event!(bool delegate(State, State)) beforeEach;
32 
33     /// The hook to be executed after a successful transition.
34     Event!(void delegate(State, State)) afterEach;
35 
36     private State _state = State.init;
37 
38     /// The active state.
39     State state() const
40     {
41         return _state;
42     }
43 
44     version (D_Ddoc)
45     {
46         /**
47          * A function for appending listeners to the state event.
48          * Can also be called using the alias on!#{stateName}, where the state name is a string.
49          *
50          * Params:
51          *  state = The state whose event to subscribe to.
52          *  listeners = The listeners to append.
53          *
54          * See_Also:
55          *  subscribed.event.Event.append
56          */
57         void on(State state)(EventType.ListenerType[] listeners...);
58 
59         /**
60          * A function for removing listeners from the state event.
61          *
62          * Params:
63          *  state = The state whose event to remove from.
64          *  listeners = The listeners to remove.
65          *
66          * See_Also:
67          *  subscribed.event.Event.append
68          */
69         void off(State state)(EventType.ListenerType[] listeners...);
70 
71         /**
72          * Calls all the registered listeners in order.
73          * Can also be called using the alias go!#{stateName}, where the state name is a string.
74          *
75          * Params:
76          *  state = The state to transition to.
77          *  params = The param tuple to call the listener with.
78          *
79          * Returns:
80          *  An array of results from the listeners.
81          *  If $(DDOC_PSYMBOL EventType.ReturnType) is void, then this function also returns void.
82          *
83          * See_Also:
84          *  subscribed.event.Event.call
85          */
86         void go(State state)(EventType.ParamTypes params);
87     }
88 
89     private mixin template bindStateTransitions(State s)
90     {
91         private EventType event;
92 
93         void on(State state : s)(EventType.ListenerType[] listeners...)
94         {
95             event.append(listeners);
96         }
97 
98         void off(State state : s)(EventType.ListenerType[] listeners...)
99         {
100             event.remove(listeners);
101         }
102 
103         void go(State state : s)(EventType.ParamTypes params)
104         {
105             auto oldState = _state;
106 
107             foreach (listener; beforeEach.listeners)
108                 if (!listener(oldState, state))
109                     return;
110 
111             event.call(params);
112             _state = state;
113             afterEach(oldState, state);
114         }
115     }
116 
117     static foreach (State s; EnumMembers!State)
118         mixin bindStateTransitions!s;
119 }
120 
121 /// An event machine stores it's state.
122 unittest
123 {
124     enum State { stateA, stateB }
125     alias Machine = EventMachine!State;
126     Machine machine;
127 
128     assert(machine.state == State.stateA, "The machine is not initially in it's default state");
129     machine.go!(State.stateB)();
130     assert(machine.state == State.stateB, "The machine does not navigate to other states");
131     machine.go!(State.stateB)();
132     assert(machine.state == State.stateB, "The machine does not permit loops");
133     machine.go!(State.stateA)();
134     assert(machine.state == State.stateA, "The machine does not navigate to other states");
135 }
136 
137 /// An event machine supports enums with negative values.
138 unittest
139 {
140     enum State { stateA = -21, stateB = 3 }
141     alias Machine = EventMachine!State;
142     Machine machine;
143 
144     assert(machine.state == State.stateA, "The machine is not initially in it's default state");
145     machine.go!(State.stateB)();
146     assert(machine.state == State.stateB, "The machine does not navigate to other states");
147 }
148 
149 /// beforeEach and afterEach run appropriately.
150 unittest
151 {
152     enum State { stateA, stateB }
153     alias Machine = EventMachine!(State, void delegate());
154     Machine machine;
155 
156     bool beforeEachRan, afterEachRan;
157 
158     machine.beforeEach ~= (oldState, newState) {
159         assert(oldState == State.stateA, "The machine doesn't move from it's initial state");
160         assert(newState == State.stateB, "The machine doesn't move to a non-initial state");
161         beforeEachRan = true;
162         return true;
163     };
164 
165     machine.afterEach ~= (oldState, newState) {
166         afterEachRan = true;
167         assert(oldState == State.stateA, "The machine doesn't move from it's initial state");
168         assert(newState == State.stateB, "The machine doesn't move to a non-initial state");
169     };
170 
171     machine.go!(State.stateB)();
172     assert(beforeEachRan, "The beforeEach hook has been ran");
173     assert(afterEachRan, "The afterEach hook has been ran");
174 }
175 
176 /// beforeEach can cancel subsequent events.
177 unittest
178 {
179     enum State { stateA, stateB }
180     alias Machine = EventMachine!(State, void delegate());
181     Machine machine;
182 
183     machine.beforeEach ~= (oldState, newState) {
184         return false;
185     };
186 
187     machine.on!(State.stateA)(() {
188         assert(false, "The machine moves to stateA despite the failing beforeEach check");
189     });
190 
191     machine.go!(State.stateA)();
192 }