1 module dcord.types.base;
2 
3 import std.conv,
4        std.typecons,
5        std.stdio,
6        std.array,
7        std.algorithm,
8        std.traits,
9        std.functional,
10        std.algorithm.setops : nWayUnion;
11 
12 import dcord.client;
13 
14 import vibe.core.core : runTask, sleep;
15 import vibe.core.sync : createManualEvent, LocalManualEvent;
16 
17 // Commonly used public imports
18 public import dcord.util.json;
19 public import std.datetime;
20 
21 immutable ulong DISCORD_EPOCH = 1420070400000;
22 
23 // TODO: Eventually this should be a type
24 alias Snowflake = ulong;
25 
26 string toString(Snowflake s) {
27   return to!string(s);
28 }
29 
30 SysTime toSysTime(Snowflake s) {
31   return SysTime(unixTimeToStdTime(cast(int)(((s >> 22) + DISCORD_EPOCH) / 1000)));
32 }
33 
34 /**
35   AsyncChainer is a utility for exposing methods that can help
36   chain actions with various delays/resolving patterns.
37 */
38 class AsyncChainer(T) {
39   private {
40     T obj;
41     AsyncChainer!T parent;
42     LocalManualEvent resolveEvent;
43     bool ignoreFailure;
44   }
45 
46   /**
47     The base constructor which handles the optional creation of ManualEvent used
48     in the case where this member of the AsyncChain has a delay (or depends on
49     something with a delay).
50 
51     Params:
52       obj = the object to wrap for chaining
53       hasResolver = if true, create a ManualEvent used for resolving
54   */
55   this(T obj, bool hasResolver = false) {
56     this.obj = obj;
57 
58     if (hasResolver) {
59       this.resolveEvent = createManualEvent();
60     }
61   }
62 
63   /**
64     Delayed constructor creates an AsyncChainer chain member which waits for
65     the specified delay before resolving the current and next members of the
66     chain.
67 
68     Params:
69       obj = the object to wrap for chaining
70       delay = a duration to delay before resolving
71       parent = the parent member in the chain to depend on before resolving
72   */
73   this(T obj, Duration delay, AsyncChainer!T parent = null) {
74     this(obj, true);
75 
76     this.parent = parent;
77 
78     runTask({
79       // If we have a parent, wait on its resolve event first
80       if (this.parent) {
81         this.parent.resolveEvent.wait();
82       }
83 
84       // Then sleep for the delay
85       sleep(delay);
86 
87       // And trigger our resolve event
88       this.resolveEvent.emit();
89     });
90   }
91 
92   private void call(string func, Args...)(Args args) {
93     if (this.ignoreFailure) {
94       try {
95         this.obj.call!(func)(args);
96       } catch (Exception e) {}
97     } else {
98       this.obj.call!(func)(args);
99     }
100   }
101 
102   /**
103     Utility method for chaining. Returns a new child member of the chain.
104   */
105   AsyncChainer!T after(Duration delay) {
106     return new AsyncChainer!T(this.obj, delay, this);
107   }
108 
109   /**
110     opDispatch override that provides a mechanisim for wrapped chaining of the
111     inner object.
112   */
113   AsyncChainer!T opDispatch(string func, Args...)(Args args) {
114     if (this.resolveEvent) {
115       auto next = new AsyncChainer!T(this.obj, true);
116 
117       runTask({
118         this.resolveEvent.wait();
119         this.call!(func)(args);
120         next.resolveEvent.emit();
121       });
122 
123       return next;
124     } else {
125       this.call!(func)(args);
126       // this.obj.call!(func)(args);
127       return this;
128     }
129   }
130 
131   AsyncChainer!T maybe() {
132     this.ignoreFailure = true;
133     return this;
134   }
135 }
136 
137 
138 
139 /**
140   Base class for all models. Provides a simple interface definition and some
141   utility constructor code.
142 */
143 class IModel {
144   @JSONIgnore
145   Client  client;
146 
147   void initialize() {};
148 
149   this() {}
150 
151   this(IModel parent, VibeJSON obj) {
152     throw new Exception(":(");
153   }
154 
155   this(Client client, VibeJSON obj) {
156     throw new Exception(":(");
157   }
158 }
159 
160 /**
161   Base template for all models. Provides utility methods for AsyncChaining and
162   a base constructor that calls the parent IModel constructor.
163 */
164 mixin template Model() {
165   this() {}
166 
167   this(IModel parent, VibeJSON obj) {
168     this.load(parent.client, obj);
169   }
170 
171   this(Client client, VibeJSON obj) {
172     this.load(client, obj);
173   }
174 
175   void load(Client client, VibeJSON obj) {
176     version (TIMING) {
177       client.log.tracef("Starting creation of model %s", this.toString);
178       auto sw = StopWatch(AutoStart.yes);
179     }
180 
181     this.client = client;
182     this.deserializeFromJSON(obj);
183     this.initialize();
184 
185     version (TIMING) {
186       this.client.log.tracef("Finished creation of model %s in %sms", this.toString,
187         sw.peek().to!("msecs", real));
188     }
189   }
190 
191   /// Allows chaining based on a delay. Returns a new AsyncChainer of this type.
192   auto after(Duration delay) {
193     return new AsyncChainer!(typeof(this))(this, delay);
194   }
195 
196   /// Allows arbitrary chaining. Returns a new AsyncChainer of this type.
197   auto chain() {
198     return new AsyncChainer!(typeof(this))(this);
199   }
200 
201   void call(string s, T...)(T args) {
202     __traits(getMember, this, s)(args);
203   }
204 
205 }
206 
207 /**
208   ModelMap serves as an abstraction layer around associative arrays that store
209   models. Usually ModelMaps will be a direct mapping of ID (Snowflake) -> Model.
210 */
211 class ModelMap(TKey, TValue) {
212   /// Underlying associative array
213   TValue[TKey]  data;
214 
215   static ModelMap!(TKey, TValue) fromJSONArray(string key)(IModel model, VibeJSON data) {
216     auto result = new ModelMap!(TKey, TValue);
217 
218     foreach (value; data) {
219       TValue v = new TValue(model.client, value);
220       result[__traits(getMember, v, key)] = v;
221     }
222 
223     return result;
224   }
225 
226   /// Set the key to a value.
227   TValue set(TKey key, TValue value) {
228     if (value is null) {
229       this.remove(key);
230       return null;
231     }
232 
233     this.data[key] = value;
234     return value;
235   }
236 
237   /// Return the value for a key. Throws an exception if the key does not exist
238   TValue get(TKey key) {
239     return this.data[key];
240   }
241 
242   /// Return the value for a key, or if it doesn't exist a specified default value
243   TValue get(TKey key, TValue def) {
244     if(this.has(key)) return this.get(key);
245     return def;
246   }
247 
248   /// Removes a key.
249   void remove(TKey key) {
250     this.data.remove(key);
251   }
252 
253   /// Returns true if the key exists within the mapping
254   bool has(TKey key) {
255     return (key in this.data) != null;
256   }
257 
258   /// Indexing by key
259   TValue opIndex(TKey key) {
260     return this.get(key);
261   }
262 
263   /// Indexing assignment
264   void opIndexAssign(TValue value, TKey key) {
265     this.set(key, value);
266   }
267 
268   /// Returns the length of the mapping
269   size_t length() {
270     return this.data.length;
271   }
272 
273   /// Returns a new mapping from a subset of keys
274   auto subset(TKey[] keysWanted) {
275     auto obj = new ModelMap!(TKey, TValue);
276 
277     foreach (k; keysWanted) {
278       obj[k] = this.get(k);
279     }
280 
281     return obj;
282   }
283 
284   /**
285     Allows using a delegate to filter the keys/values of the mapping into a new
286     mapping.
287 
288     Params:
289       f = a delegate which returns true if the passed in key/value matches
290   */
291   auto filter(bool delegate(TKey, TValue) f) {
292     return this.subset(this.data.keys.filter!(k => f(k, this.get(k))).array);
293   }
294 
295   /**
296     Allows using a delegate to filter the values of the mapping
297 
298     Params:
299       f = a delegate which returns true if the passed in value matches.
300   */
301   auto filter(bool delegate(TValue) f) {
302     return this.data.values.filter!(f);
303   }
304 
305   /**
306     Allows applying a delegate over the values of the mapping
307 
308     Params:
309       f = a delegate which is applied to each value in the mapping.
310   */
311   auto each(void delegate(TValue) f) {
312     return this.data.values.each!(f);
313   }
314 
315   /**
316     Returns a single value from the mapping, based on the return value of a
317     delegate
318 
319     Params:
320       f = a delegate which returns true if the value passed in matches.
321       def = default value to return if nothing matches.
322   */
323   TValue pick(bool delegate(TValue) f, TValue def=null) {
324     foreach (value; this.data.values) {
325       if (f(value)) {
326         return value;
327       }
328     }
329     return def;
330   }
331 
332   /// Returns an array of keys from the mapping
333   auto keys() {
334     return this.data.keys;
335   }
336 
337   /// Returns an array of values from the mapping
338   auto values() {
339     return this.data.values;
340   }
341 
342   /// Return the set-union for an array of keys
343   TKey[] keyUnion(TKey[] other) {
344     return nWayUnion([this.keys, other]).array;
345   }
346 
347   /// Allows applying an operation as a delegate to the ModelMap
348   int opApply(int delegate(ref TKey, ref TValue) dg) {
349     int result = 0;
350     foreach (a, b; this.data) {
351       result = dg(a, b);
352       if (result) break;
353     }
354     return result;
355   }
356 }