1 // A simple mediator implementation.
2 module subscribed.mediator;
3 
4 import std.traits : isCallable;
5 import std.array : replace;
6 import std.meta;
7 
8 import subscribed.event;
9 
10 /**
11  * A simple mediator implementation.
12  * More precisely, an event collection with a unified interface and beforeEach/afterEach hooks.
13  *
14  * Params:
15  *  params - A list alternating between index types and callable types.
16  */
17 struct Mediator(params...)
18 {
19     static assert(params.length % 2 == 0, "The parameter count must be even");
20 
21     /// The type of the (indexing) channel names.
22     alias IType = typeof(params[0]);
23 
24     private
25     {
26         struct Channel(IType name_, T) if (isCallable!T)
27         {
28             alias Type = T;
29             enum name = name_;
30         }
31 
32         template channels(params...)
33         {
34             static if (params.length == 0)
35             {
36                 alias channels = AliasSeq!();
37             }
38             else
39             {
40                 static assert(
41                     is(typeof(params[0]) == IType) && isCallable!(params[1]),
42                     "Parameters must alternate between index types and callable types"
43                 );
44 
45                 alias channels = AliasSeq!(Channel!(params[0], params[1]), channels!(params[2..$]));
46             }
47         }
48 
49         mixin template bindChannel(c)
50         {
51             private
52             {
53                 alias EventType = Event!(c.Type);
54                 EventType event;
55             }
56 
57             void on(IType channel : c.name)(EventType.ListenerType[] listeners...)
58             {
59                 event.append(listeners);
60             }
61 
62             void off(IType channel : c.name)(EventType.ListenerType[] listeners...)
63             {
64                 event.remove(listeners);
65             }
66 
67             void emit(IType channel : c.name)(EventType.ParamTypes params)
68             {
69                 foreach (listener; beforeEach.listeners)
70                     if (!listener(channel))
71                         return;
72 
73                 event.call(params);
74                 afterEach(channel);
75             }
76         }
77     }
78 
79     /// The hook to be executed before any transition. If false is returned, the no transition occurs.
80     Event!(bool delegate(IType)) beforeEach;
81 
82     /// The hook to be executed after a successful transition.
83     Event!(void delegate(IType)) afterEach;
84 
85     version (D_Ddoc)
86     {
87         alias EventType = Event!(void function());
88 
89         /**
90          * A function for appending listeners to the channel event.
91          *
92          * Params:
93          *  channel = The channel whose event to subscribe to.
94          *  listeners = The listeners to append.
95          *
96          * See_Also:
97          *  subscribed.event.Event.append
98          */
99         void on(IType channel)(EventType.ListenerType[] listeners...);
100 
101         /**
102          * A function for removing listeners from the channel event.
103          *
104          * Params:
105          *  channel = The channel whose event to remove from.
106          *  listeners = The listeners to remove.
107          *
108          * See_Also:
109          *  subscribed.event.Event.append
110          */
111         void off(IType channel)(EventType.ListenerType[] listeners...);
112 
113         /**
114          * Calls all the registered listeners for the channel in order.
115          *
116          * Params:
117          *  channel = The channel to emit a message to.
118          *  params = The param tuple to call the listeners with.
119          *
120          * Returns:
121          *  An array of results from the listeners.
122          *  If $(DDOC_PSYMBOL EventType.ReturnType) is void, then this function also returns void.
123          *
124          * See_Also:
125          *  subscribed.event.Event.call
126          */
127         void emit(string channel)(EventType.ParamTypes params);
128     }
129 
130     static foreach (channel; channels!params)
131         mixin bindChannel!channel;
132 }
133 
134 /// The mediator events can be strings.
135 unittest
136 {
137     Mediator!(
138         "start", void delegate(),
139         "stop", void delegate()
140     ) mediator;
141 
142     mediator.emit!"start"();
143 }
144 
145 /// The mediator events can be enum members.
146 unittest
147 {
148     enum Event { start, stop }
149 
150     Mediator!(
151         Event.start, void delegate(),
152         Event.stop, void delegate()
153     ) mediator;
154 
155     mediator.emit!(Event.start)();
156 }
157 
158 /// The mediator events cannot be of mixed type.
159 unittest
160 {
161     immutable canCompile = __traits(compiles, Mediator!(
162         "start", void delegate(),
163         3, void delegate()
164     ));
165 
166     assert(!canCompile, "Can compile mediators with mixed index types");
167 }
168 
169 /// The mediator can subscribe, unsubscribe and broadcast events.
170 unittest
171 {
172     Mediator!(
173         "inc", void delegate(),
174         "dec", void delegate(),
175         "reset counter", void delegate()
176     ) mediator;
177 
178     int counter;
179 
180     void increment()
181     {
182         counter++;
183     }
184 
185     void decrement()
186     {
187         counter--;
188     }
189 
190     void reset()
191     {
192         counter = 0;
193     }
194 
195     mediator.on!"inc"(&increment);
196     mediator.on!"dec"(&decrement);
197     mediator.on!"reset counter"(&reset);
198 
199     assert(counter == 0, "Mediator functions are called before any action is performed");
200     mediator.emit!"inc"();
201     assert(counter == 1, "The mediator does not call one of it's functions");
202     mediator.emit!"dec"();
203     assert(counter == 0, "The mediator does not call one of it's functions");
204 
205     assert(counter == 0, "Mediator functions are called before any action is performed");
206     mediator.emit!"inc"();
207     assert(counter == 1, "The mediator does not call one of it's functions");
208     mediator.emit!"reset counter"();
209     assert(counter == 0, "The mediator does not call one of it's functions");
210 
211     mediator.beforeEach ~= string => false;
212 
213     assert(counter == 0, "The beforeEach hook does not work");
214     mediator.emit!"inc"();
215     assert(counter == 0, "The beforeEach hook does not work");
216     mediator.emit!"dec"();
217     assert(counter == 0, "The beforeEach hook does not work");
218 
219     mediator.beforeEach.clear();
220 
221     mediator.off!"inc"(&increment);
222     mediator.off!"dec"(&decrement);
223 
224     assert(counter == 0, "The mediator called one of it's functions while unregistering them");
225     mediator.emit!"inc"();
226     assert(counter == 0, "The mediator did not remove a listener");
227     mediator.emit!"dec"();
228     assert(counter == 0, "The mediator did not remove a listener");
229 }