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