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