1 module dls.util.json;
2 
3 import std.algorithm : map;
4 import std.array : array;
5 import std.conv : to;
6 import std.json;
7 import std.traits;
8 import std.typecons : Nullable, nullable;
9 
10 /++
11 Converts a `JSONValue` to an object of type `T` by filling its fields with the JSON's fields.
12 +/
13 T convertFromJSON(T)(JSONValue json) if (is(T == class) || is(T == struct))
14 {
15     static if (is(T == class))
16     {
17         auto result = new T();
18     }
19     else static if (is(T == struct))
20     {
21         auto result = T();
22     }
23     else
24     {
25         static assert(false, "Cannot convert JSON to " ~ typeid(T));
26     }
27 
28     if (json.type != JSON_TYPE.OBJECT)
29     {
30         return result;
31     }
32 
33     foreach (member; __traits(allMembers, T))
34     {
35         static if (__traits(getProtection, __traits(getMember, T,
36                 member)) == "public" && !isType!(__traits(getMember, T,
37                 member)) && !isSomeFunction!(__traits(getMember, T, member)))
38         {
39             try
40             {
41                 __traits(getMember, result, member) = convertFromJSON!(typeof(__traits(getMember,
42                         result, member)))(json[normalizeMemberName(member)]);
43             }
44             catch (JSONException e)
45             {
46             }
47         }
48     }
49 
50     return result;
51 }
52 
53 version (unittest)
54 {
55     struct TestStruct
56     {
57         uint uinteger;
58         JSONValue json;
59     }
60 
61     class TestClass
62     {
63         int integer;
64         float floating;
65         string text;
66         int[] array;
67         string[string] dictionary;
68         TestStruct testStruct;
69     }
70 }
71 
72 unittest
73 {
74     const jsonString = `{
75         "integer": 42,
76         "floating": 3.0,
77         "text": "Hello world",
78         "array": [0, 1, 2],
79         "dictionary": {
80             "key1": "value1",
81             "key2": "value2",
82             "key3": "value3"
83         },
84         "testStruct": {
85             "uinteger": 16,
86             "json": {
87                 "key": "value"
88             }
89         }
90     }`;
91 
92     const testClass = convertFromJSON!TestClass(parseJSON(jsonString));
93     assert(testClass.integer == 42);
94     assert(testClass.floating == 3.0);
95     assert(testClass.text == "Hello world");
96     assert(testClass.array == [0, 1, 2]);
97     const dictionary = ["key1" : "value1", "key2" : "value2", "key3" : "value3"];
98     assert(testClass.dictionary == dictionary);
99     assert(testClass.testStruct.uinteger == 16);
100     assert(testClass.testStruct.json["key"].str == "value");
101 }
102 
103 N convertFromJSON(N : Nullable!T, T)(JSONValue json)
104 {
105     return (json.type == JSON_TYPE.NULL) ? N() : convertFromJSON!T(json).nullable;
106 }
107 
108 unittest
109 {
110     auto json = JSONValue(42);
111     auto result = convertFromJSON!(Nullable!int)(json);
112     assert(!result.isNull && result.get() == json.integer);
113 
114     json = JSONValue(null);
115     assert(convertFromJSON!(Nullable!int)(json).isNull);
116 }
117 
118 T convertFromJSON(T : JSONValue)(JSONValue json)
119 {
120     return json.nullable;
121 }
122 
123 unittest
124 {
125     assert(convertFromJSON!JSONValue(JSONValue(42)) == JSONValue(42));
126 }
127 
128 T convertFromJSON(T)(JSONValue json) if (isNumeric!T)
129 {
130     switch (json.type)
131     {
132     case JSON_TYPE.NULL:
133     case JSON_TYPE.FALSE:
134         return 0.to!T;
135 
136     case JSON_TYPE.TRUE:
137         return 1.to!T;
138 
139     case JSON_TYPE.FLOAT:
140         return json.floating.to!T;
141 
142     case JSON_TYPE.INTEGER:
143         return json.integer.to!T;
144 
145     case JSON_TYPE.UINTEGER:
146         return json.uinteger.to!T;
147 
148     case JSON_TYPE.STRING:
149         return json.str.to!T;
150 
151     default:
152         throw new JSONException(json.toString() ~ " is not a numeric type");
153     }
154 }
155 
156 unittest
157 {
158     assert(convertFromJSON!float(JSONValue(3.0)) == 3.0);
159     assert(convertFromJSON!int(JSONValue(42)) == 42);
160     assert(convertFromJSON!uint(JSONValue(42U)) == 42U);
161 
162     // quirky JSON cases
163 
164     assert(convertFromJSON!int(JSONValue(null)) == 0);
165     assert(convertFromJSON!int(JSONValue(false)) == 0);
166     assert(convertFromJSON!int(JSONValue(true)) == 1);
167     assert(convertFromJSON!int(JSONValue("42")) == 42);
168 }
169 
170 T convertFromJSON(T)(JSONValue json) if (isBoolean!T)
171 {
172     switch (json.type)
173     {
174     case JSON_TYPE.NULL:
175     case JSON_TYPE.FALSE:
176         return false;
177 
178     case JSON_TYPE.FLOAT:
179         return json.floating != 0;
180 
181     case JSON_TYPE.INTEGER:
182         return json.integer != 0;
183 
184     case JSON_TYPE.UINTEGER:
185         return json.uinteger != 0;
186 
187     default:
188         return true;
189     }
190 }
191 
192 unittest
193 {
194     assert(convertFromJSON!bool(JSONValue(false)) == false);
195     assert(convertFromJSON!bool(JSONValue(true)) == true);
196 
197     // quirky JSON cases
198 
199     assert(convertFromJSON!bool(JSONValue(null)) == false);
200     assert(convertFromJSON!bool(JSONValue(0.0)) == false);
201     assert(convertFromJSON!bool(JSONValue(0)) == false);
202     assert(convertFromJSON!bool(JSONValue(0U)) == false);
203 
204     assert(convertFromJSON!bool(JSONValue(new int[0])) == true);
205     assert(convertFromJSON!bool(JSONValue(3.0)) == true);
206     assert(convertFromJSON!bool(JSONValue(42)) == true);
207     assert(convertFromJSON!bool(JSONValue(42U)) == true);
208 }
209 
210 T convertFromJSON(T)(JSONValue json)
211         if (isSomeChar!T || isSomeString!T || is(T : string) || is(T : wstring) || is(T : dstring))
212 {
213     switch (json.type)
214     {
215         static if (!is(T == enum))
216         {
217             static if (!isSomeChar!T)
218             {
219     case JSON_TYPE.NULL:
220                 return "null".to!T;
221 
222     case JSON_TYPE.FALSE:
223                 return "false".to!T;
224 
225     case JSON_TYPE.TRUE:
226                 return "true".to!T;
227             }
228 
229     case JSON_TYPE.FLOAT:
230             return json.floating.to!T;
231 
232     case JSON_TYPE.INTEGER:
233             return json.integer.to!T;
234 
235     case JSON_TYPE.UINTEGER:
236             return json.uinteger.to!T;
237 
238     case JSON_TYPE.OBJECT:
239     case JSON_TYPE.ARRAY:
240             return json.toString().to!T;
241         }
242 
243     case JSON_TYPE.STRING:
244         return json.str.to!T;
245 
246     default:
247         throw new JSONException(json.toString() ~ " is not a string type");
248     }
249 }
250 
251 unittest
252 {
253     auto json = JSONValue("Hello");
254     assert(convertFromJSON!string(json) == json.str);
255     assert(convertFromJSON!(char[])(json) == json.str);
256     assert(convertFromJSON!(wchar[])(json) == "Hello"w);
257     assert(convertFromJSON!(dchar[])(json) == "Hello"d);
258 
259     // beware of the fact that JSONValue treats chars as integers; this returns "97" and not "a"
260     assert(convertFromJSON!string(JSONValue('a')) != "a");
261     assert(convertFromJSON!string(JSONValue("a")) == "a");
262 
263     assert(convertFromJSON!char(JSONValue('a')) == 'a');
264     assert(convertFromJSON!char(JSONValue("a")) == 'a');
265 
266     enum TestEnum
267     {
268         hello = "hello",
269         world = "world"
270     }
271 
272     assert(convertFromJSON!TestEnum(JSONValue("hello")) == TestEnum.hello);
273 
274     // quirky JSON cases
275 
276     assert(convertFromJSON!string(JSONValue(null)) == "null");
277     assert(convertFromJSON!string(JSONValue(false)) == "false");
278     assert(convertFromJSON!string(JSONValue(true)) == "true");
279 }
280 
281 T convertFromJSON(T : U[], U)(JSONValue json)
282         if (isArray!T && !isSomeString!T && !is(T : string) && !is(T : wstring) && !is(T : dstring))
283 {
284     switch (json.type)
285     {
286     case JSON_TYPE.NULL:
287     case JSON_TYPE.FALSE:
288     case JSON_TYPE.TRUE:
289         return [];
290 
291     case JSON_TYPE.FLOAT:
292     case JSON_TYPE.INTEGER:
293     case JSON_TYPE.UINTEGER:
294     case JSON_TYPE.STRING:
295         return [convertFromJSON!U(json)];
296 
297     case JSON_TYPE.ARRAY:
298         return json.array.map!(value => convertFromJSON!U(value)).array.to!T;
299 
300     default:
301         throw new JSONException(json.toString() ~ " is not a string type");
302     }
303 }
304 
305 unittest
306 {
307     assert(convertFromJSON!(int[])(JSONValue([0, 1, 2, 3])) == [0, 1, 2, 3]);
308 
309     // quirky JSON cases
310 
311     assert(convertFromJSON!(int[])(JSONValue(null)) == []);
312     assert(convertFromJSON!(int[])(JSONValue(false)) == []);
313     assert(convertFromJSON!(int[])(JSONValue(true)) == []);
314     assert(convertFromJSON!(float[])(JSONValue(3.0)) == [3.0]);
315     assert(convertFromJSON!(int[])(JSONValue(42)) == [42]);
316     assert(convertFromJSON!(uint[])(JSONValue(42U)) == [42U]);
317     assert(convertFromJSON!(string[])(JSONValue("Hello")) == ["Hello"]);
318 }
319 
320 T convertFromJSON(T : U[K], U, K)(JSONValue json) if (isAssociativeArray!T)
321 {
322     U[K] result;
323 
324     switch (json.type)
325     {
326     case JSON_TYPE.NULL:
327         return result;
328 
329     case JSON_TYPE.OBJECT:
330         foreach (key, value; json.object)
331         {
332             result[key.to!K] = convertFromJSON!U(value);
333         }
334 
335         break;
336 
337     case JSON_TYPE.ARRAY:
338         foreach (key, value; json.array)
339         {
340             result[key.to!K] = convertFromJSON!U(value);
341         }
342 
343         break;
344 
345     default:
346         throw new JSONException(json.toString() ~ " is not an object type");
347     }
348 
349     return result;
350 }
351 
352 unittest
353 {
354     auto dictionary = ["hello" : 42, "world" : 0];
355     assert(convertFromJSON!(int[string])(JSONValue(dictionary)) == dictionary);
356 
357     // quirky JSON cases
358 
359     assert(convertFromJSON!(int[string])(JSONValue([16, 42])) == ["0" : 16, "1" : 42]);
360     dictionary.clear();
361     assert(convertFromJSON!(int[string])(JSONValue(null)) == dictionary);
362 }
363 
364 Nullable!JSONValue convertToJSON(T)(T value)
365         if ((is(T == class) || is(T == struct)) && !is(T == JSONValue))
366 {
367     static if (is(T == class))
368     {
369         if (value is null)
370         {
371             return Nullable!JSONValue();
372         }
373     }
374 
375     auto result = JSONValue();
376 
377     foreach (member; __traits(allMembers, T))
378     {
379         static if (__traits(getProtection, __traits(getMember, T,
380                 member)) == "public" && !isType!(__traits(getMember, T,
381                 member)) && !isSomeFunction!(__traits(getMember, T, member)))
382         {
383             auto json = convertToJSON!(typeof(__traits(getMember, value, member)))(
384                     __traits(getMember, value, member));
385 
386             if (!json.isNull)
387             {
388                 result[normalizeMemberName(member)] = json.get();
389             }
390         }
391     }
392 
393     return result.nullable;
394 }
395 
396 unittest
397 {
398     auto testClass = new TestClass();
399     testClass.integer = 42;
400     testClass.floating = 3.5;
401     testClass.text = "Hello world";
402     testClass.array = [0, 1, 2];
403     testClass.dictionary = ["key1" : "value1", "key2" : "value2"];
404     testClass.testStruct = TestStruct();
405     testClass.testStruct.uinteger = 16;
406     testClass.testStruct.json = JSONValue(["key1" : "value1", "key2" : "value2"]);
407 
408     auto jsonString = `{
409         "integer": 42,
410         "floating": 3.5,
411         "text": "Hello world",
412         "array": [0, 1, 2],
413         "dictionary": {
414             "key1": "value1",
415             "key2": "value2"
416         },
417         "testStruct": {
418             "uinteger": 16,
419             "json": {
420                 "key1": "value1",
421                 "key2": "value2"
422             }
423         }
424     }`;
425 
426     auto json = convertToJSON(testClass);
427     // parseJSON() will parse `uinteger` as a regular integer, meaning that the JSON's are considered equal,
428     // even though technically they are equivalent (16 as int or as uint is technically the same value)
429     assert(json.get().toString() == parseJSON(jsonString).toString());
430 }
431 
432 Nullable!JSONValue convertToJSON(N : Nullable!T, T)(N value)
433 {
434     return value.isNull ? Nullable!JSONValue() : convertToJSON!T(value.get());
435 }
436 
437 unittest
438 {
439     assert(convertToJSON(Nullable!int()) == Nullable!JSONValue());
440     assert(convertToJSON(Nullable!int(42)) == JSONValue(42));
441 }
442 
443 Nullable!JSONValue convertToJSON(T)(T value)
444         if ((!is(T == class) && !is(T == struct)) || is(T == JSONValue))
445 {
446     return JSONValue(value).nullable;
447 }
448 
449 unittest
450 {
451     assert(convertToJSON(3.0) == JSONValue(3.0));
452     assert(convertToJSON(42) == JSONValue(42));
453     assert(convertToJSON(42U) == JSONValue(42U));
454     assert(convertToJSON(false) == JSONValue(false));
455     assert(convertToJSON(true) == JSONValue(true));
456     assert(convertToJSON('a') == JSONValue('a'));
457     assert(convertToJSON("Hello world") == JSONValue("Hello world"));
458     assert(convertToJSON(JSONValue(42)) == JSONValue(42));
459 }
460 
461 Nullable!JSONValue convertToJSON(T : U[], U)(T value)
462         if (isArray!T && !isSomeString!T)
463 {
464     return JSONValue(value.map!(item => convertToJSON(item))()
465             .map!(json => json.isNull ? JSONValue(null) : json)().array).nullable;
466 }
467 
468 unittest
469 {
470     assert(convertToJSON([0, 1, 2]) == JSONValue([0, 1, 2]));
471     assert(convertToJSON(["hello", "world"]) == JSONValue(["hello", "world"]));
472 }
473 
474 Nullable!JSONValue convertToJSON(T : U[string], U)(T value)
475         if (isAssociativeArray!T)
476 {
477     auto result = JSONValue();
478 
479     foreach (key; value.keys)
480     {
481         auto json = convertToJSON(value[key]);
482         result[key] = json.isNull ? JSONValue(null) : json;
483     }
484 
485     return result.nullable;
486 }
487 
488 unittest
489 {
490     auto json = convertToJSON(["hello" : 16, "world" : 42]);
491     assert(!json.isNull);
492     assert(json["hello"].integer == 16);
493     assert(json["world"].integer == 42);
494 }
495 
496 /++
497 Removes underscores from names. Some protocol variable names can be reserved names (like `version`) and thus have an
498 added underscore in their protocol definition.
499 +/
500 private auto normalizeMemberName(string name)
501 {
502     import std.string : endsWith;
503 
504     return name.endsWith('_') ? name[0 .. $ - 1] : name;
505 }
506 
507 unittest
508 {
509     assert(normalizeMemberName("hello") == "hello");
510     assert(normalizeMemberName("hello_") == "hello");
511     assert(normalizeMemberName("_hello") == "_hello");
512     assert(normalizeMemberName("hel_lo") == "hel_lo");
513 }