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.map!(value => convertFromJSON!U(value)).array.to!T;
295 
296     default:
297         throw new JSONException(json.toString() ~ " is not a string type");
298     }
299 }
300 
301 unittest
302 {
303     assert(convertFromJSON!(int[])(JSONValue([0, 1, 2, 3])) == [0, 1, 2, 3]);
304 
305     // quirky JSON cases
306 
307     assert(convertFromJSON!(int[])(JSONValue(null)) == []);
308     assert(convertFromJSON!(int[])(JSONValue(false)) == []);
309     assert(convertFromJSON!(int[])(JSONValue(true)) == []);
310     assert(convertFromJSON!(float[])(JSONValue(3.0)) == [3.0]);
311     assert(convertFromJSON!(int[])(JSONValue(42)) == [42]);
312     assert(convertFromJSON!(uint[])(JSONValue(42U)) == [42U]);
313     assert(convertFromJSON!(string[])(JSONValue("Hello")) == ["Hello"]);
314 }
315 
316 T convertFromJSON(T : U[K], U, K)(JSONValue json) if (isAssociativeArray!T)
317 {
318     U[K] result;
319 
320     switch (json.type)
321     {
322     case JSON_TYPE.NULL:
323         return result;
324 
325     case JSON_TYPE.OBJECT:
326         foreach (key, value; json.object)
327         {
328             result[key.to!K] = convertFromJSON!U(value);
329         }
330 
331         break;
332 
333     case JSON_TYPE.ARRAY:
334         foreach (key, value; json.array)
335         {
336             result[key.to!K] = convertFromJSON!U(value);
337         }
338 
339         break;
340 
341     default:
342         throw new JSONException(json.toString() ~ " is not an object type");
343     }
344 
345     return result;
346 }
347 
348 unittest
349 {
350     auto dictionary = ["hello" : 42, "world" : 0];
351     assert(convertFromJSON!(int[string])(JSONValue(dictionary)) == dictionary);
352 
353     // quirky JSON cases
354 
355     assert(convertFromJSON!(int[string])(JSONValue([16, 42])) == ["0" : 16, "1" : 42]);
356     dictionary.clear();
357     assert(convertFromJSON!(int[string])(JSONValue(null)) == dictionary);
358 }
359 
360 Nullable!JSONValue convertToJSON(T)(T value)
361         if ((is(T == class) || is(T == struct)) && !is(T == JSONValue))
362 {
363     static if (is(T == class))
364     {
365         if (value is null)
366         {
367             return JSONValue(null).nullable;
368         }
369     }
370 
371     auto result = JSONValue();
372 
373     foreach (member; __traits(allMembers, T))
374     {
375         static if (__traits(getProtection, __traits(getMember, T,
376                 member)) == "public" && !isType!(__traits(getMember, T,
377                 member)) && !isSomeFunction!(__traits(getMember, T, member)))
378         {
379             auto json = convertToJSON!(typeof(__traits(getMember, value, member)))(
380                     __traits(getMember, value, member));
381 
382             if (!json.isNull)
383             {
384                 result[normalizeMemberName(member)] = json.get();
385             }
386         }
387     }
388 
389     return result.nullable;
390 }
391 
392 unittest
393 {
394     import std.json : parseJSON;
395 
396     auto testClass = new TestClass();
397     testClass.integer = 42;
398     testClass.floating = 3.5;
399     testClass.text = "Hello world";
400     testClass.array = [0, 1, 2];
401     testClass.dictionary = ["key1" : "value1", "key2" : "value2"];
402     testClass.testStruct = TestStruct();
403     testClass.testStruct.uinteger = 16;
404     testClass.testStruct.json = JSONValue(["key1" : "value1", "key2" : "value2"]);
405 
406     auto jsonString = `{
407         "integer": 42,
408         "floating": 3.5,
409         "text": "Hello world",
410         "array": [0, 1, 2],
411         "dictionary": {
412             "key1": "value1",
413             "key2": "value2"
414         },
415         "testStruct": {
416             "uinteger": 16,
417             "json": {
418                 "key1": "value1",
419                 "key2": "value2"
420             }
421         }
422     }`;
423 
424     auto json = convertToJSON(testClass);
425     // parseJSON() will parse `uinteger` as a regular integer, meaning that the JSON's are considered equal,
426     // even though technically they are equivalent (16 as int or as uint is technically the same value)
427     assert(json.get().toString() == parseJSON(jsonString).toString());
428 
429     TestClass nullTestClass = null;
430     auto nullJson = convertToJSON(nullTestClass);
431     assert(!nullJson.isNull && nullJson.get().isNull);
432 }
433 
434 Nullable!JSONValue convertToJSON(N : Nullable!T, T)(N value)
435 {
436     return value.isNull ? Nullable!JSONValue() : convertToJSON!T(value.get());
437 }
438 
439 unittest
440 {
441     assert(convertToJSON(Nullable!int()) == Nullable!JSONValue());
442     assert(convertToJSON(Nullable!int(42)) == JSONValue(42));
443 }
444 
445 Nullable!JSONValue convertToJSON(T)(T value)
446         if ((!is(T == class) && !is(T == struct)) || is(T == JSONValue))
447 {
448     return JSONValue(value).nullable;
449 }
450 
451 unittest
452 {
453     assert(convertToJSON(3.0) == JSONValue(3.0));
454     assert(convertToJSON(42) == JSONValue(42));
455     assert(convertToJSON(42U) == JSONValue(42U));
456     assert(convertToJSON(false) == JSONValue(false));
457     assert(convertToJSON(true) == JSONValue(true));
458     assert(convertToJSON('a') == JSONValue('a'));
459     assert(convertToJSON("Hello world") == JSONValue("Hello world"));
460     assert(convertToJSON(JSONValue(42)) == JSONValue(42));
461 }
462 
463 Nullable!JSONValue convertToJSON(T : U[], U)(T value)
464         if (isArray!T && !isSomeString!T)
465 {
466     return JSONValue(value.map!(item => convertToJSON(item))()
467             .map!(json => json.isNull ? JSONValue(null) : json)().array).nullable;
468 }
469 
470 unittest
471 {
472     assert(convertToJSON([0, 1, 2]) == JSONValue([0, 1, 2]));
473     assert(convertToJSON(["hello", "world"]) == JSONValue(["hello", "world"]));
474 }
475 
476 Nullable!JSONValue convertToJSON(T : U[string], U)(T value)
477         if (isAssociativeArray!T)
478 {
479     auto result = JSONValue();
480 
481     foreach (key; value.keys)
482     {
483         auto json = convertToJSON(value[key]);
484         result[key] = json.isNull ? JSONValue(null) : json;
485     }
486 
487     return result.nullable;
488 }
489 
490 unittest
491 {
492     auto json = convertToJSON(["hello" : 16, "world" : 42]);
493     assert(!json.isNull);
494     assert(json["hello"].integer == 16);
495     assert(json["world"].integer == 42);
496 }
497 
498 /++
499 Removes underscores from names. Some protocol variable names can be reserved names (like `version`) and thus have an
500 added underscore in their protocol definition.
501 +/
502 private auto normalizeMemberName(string name)
503 {
504     import std..string : endsWith;
505 
506     return name.endsWith('_') ? name[0 .. $ - 1] : name;
507 }
508 
509 unittest
510 {
511     assert(normalizeMemberName("hello") == "hello");
512     assert(normalizeMemberName("hello_") == "hello");
513     assert(normalizeMemberName("_hello") == "_hello");
514     assert(normalizeMemberName("hel_lo") == "hel_lo");
515 }