1 /// An event structure representing a one-to-many function/delegate relationship.
2 module subscribed.event;
3 
4 import std.traits : isCallable, ParameterTypeTuple, ReturnTypeTpl = ReturnType;
5 import std.experimental.allocator;
6 
7 import subscribed.slist;
8 
9 /**
10  * An event structure representing a one-to-many function/delegate relationship.
11  * It mimics a function by overriding the call operator.
12  * It also implements the bidirectional range interface.
13  *
14  * Params:
15  *  T = The listener type this event contains.
16  */
17 struct Event(T) if (isCallable!T)
18 {
19     /// The listeners' type.
20     alias ListenerType = T;
21 
22     version (D_Ddoc)
23     {
24         /// The event's return type.
25         alias ReturnType = void;
26     }
27     else
28     {
29         static if (is(ReturnTypeTpl!T == void))
30             alias ReturnType = void;
31         else
32             alias ReturnType = ReturnTypeTpl!T[];
33     }
34 
35     /// The event's argument type tuple.
36     alias ParamTypes = ParameterTypeTuple!T;
37 
38     private
39     {
40         SList!T _listeners;
41         int _size;
42     }
43 
44     ~this()
45     {
46         _listeners.destroy();
47     }
48 
49     /// The number of listeners.
50     size_t size() const
51     {
52         return _size;
53     }
54 
55     /// A range array of all the listeners.
56     auto listeners()
57     {
58         return _listeners;
59     }
60 
61     /**
62      * A boolean property indicating whether there are listeners.
63      * Part of the bidirectional range interface.
64      */
65     bool empty() const
66     {
67         return _listeners.empty;
68     }
69 
70     /**
71      * Get the first listener or throw an error if there are no listeners.
72      * Part of the bidirectional range interface.
73      */
74     T front() const
75     {
76         return _listeners.front;
77     }
78 
79     /**
80      * Remove the first listener or throw an error if there are no listeners.
81      * Part of the bidirectional range interface.
82      */
83     void popFront()
84     {
85         _listeners.popFront();
86         _size -= 1;
87     }
88 
89     /**
90      * Prepend listeners to the listener collection.
91      *
92      * Params:
93      *  listeners = The listeners to insert.
94      */
95     void prepend(T[] listeners...)
96     {
97         foreach (listener; listeners)
98         {
99             _size++;
100             _listeners.insertFront(listener);
101         }
102     }
103 
104     /**
105      * Get the last listener or throw an error if there are no listeners.
106      * Part of the bidirectional range interface.
107      */
108     T back() const
109     {
110         return _listeners.back;
111     }
112 
113     /**
114      * Remove the last listener or throw an error if there are no listeners.
115      * Part of the bidirectional range interface.
116      */
117     void popBack()
118     {
119         _listeners.popBack();
120         _size -= 1;
121     }
122 
123     /**
124      * Appends a listener to the listener collection.
125      *
126      * Params:
127      *  listeners = The listeners to insert.
128      */
129     void append(T[] listeners...)
130     {
131         foreach (listener; listeners)
132         {
133             _size++;
134             _listeners.insertBack(listener);
135         }
136     }
137 
138     /**
139      * Removed all occurrences of the given listeners.
140      *
141      * Params:
142      *  listeners = The listeners to remove.
143      */
144     void remove(T[] listeners...)
145     {
146         foreach (listener; listeners)
147             _size -= _listeners.removeAll(listener);
148     }
149 
150     /**
151      * Clears all listeners.
152      */
153     void clear()
154     {
155         _listeners.clear();
156         _size = 0;
157     }
158 
159     /**
160      * Calls all the registered listeners in order.
161      *
162      * Params:
163      *  params = The param tuple to call the listener with.
164      */
165     void call(ParamTypes params)
166     {
167         foreach (listener; _listeners)
168             listener(params);
169     }
170 
171     /// Aliases $(DDOC_PSYMBOL call).
172     void opCall(ParamTypes params)
173     {
174         return call(params);
175     }
176 
177     /**
178      * Copies the event to allow multiple range-like iteration.
179      * Part of the bidirectional range interface.
180      */
181     auto save()
182     {
183         return this;
184     }
185 
186     ///
187     auto opSlice()
188     {
189         return this;
190     }
191 
192     /// Aliases $(DDOC_PSYMBOL append).
193     void opOpAssign(string s: "~")(T listener)
194     {
195         append(listener);
196     }
197 
198     /// Aliases $(DDOC_PSYMBOL remove).
199     void opOpAssign(string s: "-")(T listener)
200     {
201         remove(listener);
202     }
203 }
204 
205 version (unittest)
206 {
207     int add(int a, int b)
208     {
209         return a + b;
210     }
211 
212     int multiply(int a, int b)
213     {
214         return a * b;
215     }
216 
217     void doNothing() {}
218 }
219 
220 /// Events return all their listener outputs in dynamic arrays.
221 unittest
222 {
223     import std.array : array;
224     Event!(int function(int, int)) event;
225     event.append(&add, &multiply);
226     assert(event.front()(5, 5) == 10, "The event listeners are iterated in the same order they were added in");
227     event.popFront();
228     assert(event.front()(5, 5) == 25, "The event listeners are iterated in the same order they were added in");
229 }
230 
231 /// Adding and removing listeners is straightforward.
232 unittest
233 {
234     Event!(int function(int, int)) event;
235     event.append(&add, &multiply);
236     assert(event.size == 2, "The eveEvent!(void function())Event!(void function())nt listener count does not increase upon addition");
237     event.remove(&add, &multiply);
238     assert(event.size == 0, "The event listener count does not decrease upon removal");
239 }
240 
241 /// You can add the same listener multiple times. When removing it however, all matching listeners get removed.
242 unittest
243 {
244     Event!(void function()) event;
245 
246     event ~= &doNothing;
247     event.prepend(&doNothing);
248     event.append(&doNothing);
249     assert(event.size == 3, "The event listener does not add identical listeners");
250     event.remove(&doNothing);
251     assert(event.empty, "The event listener does not remove identical listeners");
252 }
253 
254 /// The event is a bidirectional range of it's listeners.
255 unittest
256 {
257     import std.range : isBidirectionalRange;
258     assert(isBidirectionalRange!(Event!(void function())), "The bidirectional range interface is not implemented");
259 
260     Event!(void function()) event;
261     event ~= &doNothing;
262     const checkpoint = event;
263 
264     foreach (listener; event) {}
265 
266     assert(!event.empty, "The range is cleared after foreach iteration");
267 
268     for (; !event.empty; event.popFront()) {}
269 
270     assert(event.empty, "The range is not cleared after manual iteration");
271     assert(!checkpoint.empty, "The range checkpoint is cleared after iteration");
272 }
273 
274 /// Range mutation primitives work.
275 unittest
276 {
277     import std.exception : assertThrown;
278 
279     Event!(void function()) event;
280 
281     event ~= &doNothing;
282     assert(event.front == &doNothing, "Returns the last remaining listener");
283     event.popFront();
284     assertThrown(event.back, "Getting the back of an empty event does not throw");
285     assertThrown(event.popBack(), "Popping an empty event does not throw");
286 }
287 
288 /// In case you want to remove all listeners.
289 unittest
290 {
291     Event!(void function()) event;
292 
293     event ~= &doNothing;
294     assert(!event.empty, "The event has no listeners, one listener expected");
295     event.clear();
296     assert(event.empty, "The event is not empty");
297 }