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 }