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