1 
2 module hashes;
3 
4 import std.algorithm;
5 import std.conv;
6 import std.stdio;
7 import std.traits;
8 import std.variant;
9 
10 /++
11  + Tests if the given type is a hash.
12  +/
13 enum isHash(Type : Hash!args, args...) = true;
14 /++
15  + Ditto.
16  +/
17 enum isHash(Type) = false;
18 
19 /++
20  + A templated datatype which stores key => value pairs.
21  + Keys and values stored in hash need not be unique.
22  +
23  + Querying hashes can be done with both compile-time and run-time values.
24  + ---
25  + Hash!(a => 1) hash;
26  +
27  + assert(hash["a"] == Variant(1));
28  + assert(hash.value!"a" == 1);
29  + ---
30  +
31  + Hashes also allow for duplicate keys, and value selection by type.
32  + ---
33  + int xInt;
34  + string xString;
35  + Hash!(x => 5, x => "10") hash;
36  +
37  + hash.get("x", xInt);    // Or xInt    = hash.value!("x", int);
38  + hash.get("x", xString); // Or xString = hash.value!("x", string);
39  +
40  + assert(xInt == 5);
41  + assert(xString == "10");
42  + ---
43  +/
44 struct Hash(args...)
45 {
46     // Internal use only.
47     private struct HashType
48     {
49         // Nothing.
50     }
51 
52     // Internal use only.
53     private enum _valid = _isValidArgs;
54     private static bool _isValidArgs()
55     {
56         enum invalidHashKey = "Type `%s` in Hash[index => %d] is not a valid key.";
57 
58         foreach(index, arg; args)
59         {
60             static if(!is(typeof(arg!HashType)))
61             {
62                 import std.string : format;
63                 static assert(false, invalidHashKey.format(typeof(arg).stringof, index + 1));
64                 return false;
65             }
66         }
67 
68         return true;
69     }
70 
71     /++
72      + Applies the hash onto a value which is a class or struct given by `T`.
73      +
74      + Fields in the destination value are assigned for which there exists
75      + an element with a matching key and type.
76      +
77      + For example,
78      + ---
79      + struct User
80      + {
81      +     string name;
82      +     int age;
83      +     bool admin;
84      +     bool active;
85      + }
86      +
87      + User user;
88      + Hash!(name => "Jesse", age => 24, active => "yes").apply(user);
89      +
90      + assert(user.name == "Jesse"); // name => "Jesse"
91      + assert(user.age  == 24);      // age  => 24
92      + assert(user.admin == false);  // Unchanged (no key)
93      + assert(user.active == false); // Unchanged (type mismatch)
94      + ---
95      +/
96     static T apply(T)(auto ref T dest) if(is(T == class) || is(T == struct))
97     {
98         foreach(member; __traits(allMembers, T))
99         {
100             static if(hasKey(member))
101             {
102                 alias T = typeof(__traits(getMember, dest, member));
103                 __traits(getMember, dest, member) = value!(member, T);
104             }
105         }
106 
107         return dest;
108     }
109 
110     /++
111      + Concatenates two hashes. Duplicate keys and values are preserved.
112      +/
113     static auto concat(other...)(Hash!other)
114     {
115         return Hash!(args, other).init;
116     }
117 
118     /++
119      + Tests for an empty hash.
120      +/
121     @property
122     enum bool empty = args.length == 0;
123 
124     /++
125      + Fetches an element by its runtime name and stores it into the
126      + destination parameter.
127      +/
128     static bool get(T)(string name, out T dest)
129     {
130         foreach(arg; args)
131         {
132             alias key = arg!HashType;
133             static if(is(FunctionTypeOf!key Types == __parameters))
134             {
135                 static if(isAssignable!(T, typeof(key(HashType.init))))
136                 {
137                     if(name == __traits(identifier, Types))
138                     {
139                         dest = key(HashType.init);
140                         return true;
141                     }
142                 }
143                 else static if(is(T == Variant))
144                 {
145                     if(name == __traits(identifier, Types))
146                     {
147                         dest = Variant(key(HashType.init));
148                         return true;
149                     }
150                 }
151             }
152         }
153 
154         return false;
155     }
156 
157     /++
158      + Checks for the presence of a key in the hash.
159      +/
160     static bool hasKey()(string name)
161     {
162         return keys.countUntil(name) != -1;
163     }
164 
165     /++
166      + Ditto, but accepts a template parameter.
167      +/
168     enum bool hasKey(string name) = hasKey(name);
169 
170     /++
171      + Unique set of key names in the hash. The order of keys is unspecified.
172      +/
173     @property
174     enum string[] keys = _keys;
175 
176     private static string[] _keys()
177     {
178         bool[string] keySet;
179 
180         foreach(arg; args)
181         {
182             alias key = arg!HashType;
183             static if(is(FunctionTypeOf!key Types == __parameters))
184             {
185                 enum name = __traits(identifier, Types);
186                 if(name !in keySet)
187                 {
188                     keySet[name] = true;
189                 }
190             }
191         }
192 
193         return keySet.keys;
194     }
195 
196     /++
197      + Returns the number of values in the hash.
198      +/
199     @property
200     enum size_t length = args.length;
201 
202     /++
203      + Iterates over the values in the hash as Variants.
204      +/
205     static int opApply(scope int delegate(Variant) dg)
206     {
207         foreach(arg; args)
208         {
209             alias key = arg!HashType;
210             static if(is(FunctionTypeOf!key Types == __parameters))
211             {
212                 auto value = Variant(key(HashType.init));
213 
214                 if(int result = dg(value))
215                 {
216                     return result;
217                 }
218             }
219         }
220 
221         return 0;
222     }
223 
224     /++
225      + Concatenates two hashes. Duplicate keys and values are preserved.
226      +/
227     static auto opBinary(string op : "~", other...)(Hash!other o)
228     {
229         return concat(o);
230     }
231 
232     /++
233      + Ditto, but also iterates over keys.
234      +/
235     static int opApply(scope int delegate(string, Variant) dg)
236     {
237         foreach(arg; args)
238         {
239             alias key = arg!HashType;
240             static if(is(FunctionTypeOf!key Types == __parameters))
241             {
242                 enum name  = __traits(identifier, Types);
243                 auto value = Variant(key(HashType.init));
244 
245                 if(int result = dg(name, value))
246                 {
247                     return result;
248                 }
249             }
250         }
251 
252         return 0;
253     }
254 
255     /++
256      + Fetches a value from the hash as a Variant.
257      +/
258     static Variant opIndex(string name)
259     {
260         Variant value;
261         get(name, value);
262         return value;
263     }
264 
265     /++
266      + Returns a value from the hash by name.
267      +
268      + If the type parameter `T` is given, the value returned must match `T`.
269      + If no values with with matching a matching key and type exist within the
270      + hash, `T.init` is returned instead.
271      +
272      + Params:
273      +   name = The name of the key to search for.
274      +   T    = An optional type constraint.
275      +/
276     @property
277     static auto value(string name, T...)() if(hasKey(name) && T.length < 2)
278     {
279         static if(T.length == 1)
280         {
281             T[0] result = T[0].init;
282         }
283 
284         foreach(arg; args)
285         {
286             alias key = arg!HashType;
287             static if(is(FunctionTypeOf!key Types == __parameters))
288             {
289                 static if(name == __traits(identifier, Types))
290                 {
291                     static if(T.length == 0)
292                     {
293                         return key(HashType.init);
294                     }
295                     else static if(isAssignable!(T, typeof(key(HashType.init))))
296                     {
297                         result = key(HashType.init);
298                     }
299                 }
300             }
301         }
302 
303         static if(T.length == 1)
304         {
305             return result;
306         }
307     }
308 
309     /++
310      + Returns all values in the hash converted to a type given by `T`.
311      +/
312     @property
313     static T[] values(T = Variant)()
314     {
315         T[] values;
316 
317         foreach(arg; args)
318         {
319             alias key = arg!HashType;
320             values   ~= to!T(key(HashType.init));
321         }
322 
323         return values;
324     }
325 }
326 
327 /++
328  + Automatically initializes static fields from Hash-style parameters.
329  + Only valid in structs and classes.
330  +/
331 mixin template Hashify(args...)
332 {
333     @property
334     enum hashof = Hash!args.init;
335 
336     static auto opDispatch(string op)() if(hashof.hasKey(op))
337     {
338         return hashof.value!op;
339     }
340 }
341 
342 /++
343  + Shortcut for `Hash!(args).init`
344  +/
345 auto hash(args...)()
346 {
347     return Hash!(args).init;
348 }
349 
350 unittest
351 {
352     assert( isHash!(Hash!(a => 1, b => 2)));
353     assert(!isHash!(bool));
354     assert(!isHash!(string));
355     assert(!isHash!(bool[string]));
356     assert( isHash!(Hash!()));
357     assert( isHash!(Hash!(a => 1, a => 2, a => 3)));
358     assert(!isHash!(Object));
359 }
360 
361 unittest
362 {
363     assert(!__traits(compiles, {
364         Hash!(
365             a => 1,
366             b => 2,
367             c => 3,
368             false
369         ) hash;
370     }));
371 }
372 
373 unittest
374 {
375     int x, y, z;
376     Hash!(
377         x => 1,
378         y => 2,
379         z => 3
380     ) hash;
381 
382     assert(hash.hasKey("x"));
383     assert(hash.hasKey("y"));
384     assert(hash.hasKey("z"));
385 
386     assert(hash.length == 3);
387     assert(hash.keys   == [ "x", "y", "z" ]);
388 
389     assert(hash.values        == [ Variant(1), Variant(2), Variant(3) ]);
390     assert(hash.values!string == [ "1", "2", "3" ]);
391     assert(hash.values!int    == [ 1, 2, 3 ]);
392 
393     assert(hash.get("x", x));
394     assert(hash.get("y", y));
395     assert(hash.get("z", z));
396 
397     assert(hash["x"] == Variant(1));
398     assert(hash["y"] == Variant(2));
399     assert(hash["z"] == Variant(3));
400 
401     assert(x == 1);
402     assert(y == 2);
403     assert(z == 3);
404 }
405 
406 unittest
407 {
408     auto test = hash!(
409         x => "foo",
410         y => "bar",
411         z => hash!(
412             a => 1,
413             b => 2
414         )
415     );
416 
417     assert(test.value!"x" == "foo");
418     assert(test.value!"y" == "bar");
419     assert(test.value!"z".value!"a" == 1);
420     assert(test.value!"z".value!"b" == 2);
421 }
422 
423 unittest
424 {
425     struct Test
426     {
427         int x;
428         int y;
429         int z;
430     }
431 
432     Test test;
433     Hash!(
434         x => 1,
435         y => 2,
436         z => 3
437     ) hash;
438 
439     hash.apply(test);
440     assert(test.x == 1);
441     assert(test.y == 2);
442     assert(test.z == 3);
443 }
444 
445 unittest
446 {
447     static struct Test(args...)
448     {
449         mixin Hashify!args;
450 
451         int a = hashof.value!"a";
452         int b = hashof.value!"b";
453         int c = hashof.value!"c";
454     }
455 
456     Test!(
457         a => 1,
458         b => 2,
459         c => 3
460     ) test;
461 
462     assert(test.a == 1);
463     assert(test.b == 2);
464     assert(test.c == 3);
465 }
466 
467 unittest
468 {
469     static struct Column(args...)
470     {
471         mixin Hashify!args;
472 
473         string name   = hashof.value!("name", string);
474         bool nullable = hashof.value!("nullable", bool);
475     }
476 
477     class User
478     {
479         @Column!(
480             name => "nick_name",
481             nullable => true
482         )
483         string nickname;
484     }
485 
486     import std.meta : Alias;
487     static assert(__traits(getAttributes, User.nickname).length == 1);
488     alias NicknameColumn = Alias!(__traits(getAttributes, User.nickname)[0]);
489 
490     NicknameColumn column;
491     assert(column.name == "nick_name");
492     assert(column.nullable == true);
493 }
494 
495 unittest
496 {
497     struct Test
498     {
499         int x;
500         int y;
501         int z;
502 
503         this(H)(H hash) if(isHash!H)
504         {
505             hash.apply(this);
506         }
507     }
508 
509     Test test = Test(hash!(
510         x => 2,
511         y => 3,
512         z => 4
513     ));
514 
515     assert(test.x == 2);
516     assert(test.y == 3);
517     assert(test.z == 4);
518 }
519 
520 unittest
521 {
522     auto a = hash!(a => 1, b => 2);
523     auto b = hash!(b => 3, c => 4);
524     auto c = a ~ b;
525 
526     assert(c.length == 4);
527     assert(c.values!int == [ 1, 2, 3, 4 ]);
528 }