The OpenD Programming Language

1 /+
2     == serialization.json ==
3     Copyright Alexey Drozhzhin aka Grim Maple 2024
4     Distributed under the Boost Software License, Version 1.0.
5 +/
6 /++
7     This is a JSON serializer for OpenD programming language.
8 
9     $(PITFALL
10         JSON Serializer relies on `new` to create arrays and objects, so it's incompatible
11         with `@nogc` and `betterC` code. Limited compatibility might be provided if no
12         arrays / objects are used in serializable object
13     )
14 
15     ## Usage examples
16     To use this serializer, annotate any field you want serialized with [serializable].
17     JSON serializator will attempt to automatically convert primitives to corresponding
18     JSON tupes, such as any of the number types to JSON Number, string to JSON String,
19     bool to JSON true/false.
20 
21     ---
22     struct Foo
23     {
24         @serializable int bar;
25         @serializable string baz;
26     }
27 
28     string test = serializeToJSONString(Foo());
29     ---
30 
31     By default, serializer will skip serializing fields that are `null`.
32     If you want to ensure that a ceratin field exists in the JSON object, use [jsonRequired]
33     attribute:
34     ---
35     struct Foo
36     {
37         @serializable object bar;
38         @serializable @jsonRequired object baz;
39     }
40 
41     assert(serializeToJSONString(Foo()) == "{\"baz\": null}")
42     ---
43 
44     Marking a field with [jsonRequired] will also result in an error if a required field was
45     missing when deserializing:
46 
47     ---
48     struct Foo
49     {
50         @serializeable @jsonRequired object bar;
51     }
52 
53     deserializeJSONFromString!Foo("{}"); // Error
54     ---
55 +/
56 module odc.serialization.json;
57 
58 import std.traits;
59 import std.conv : to;
60 
61 import std.exception : assertThrown, assertNotThrown;
62 import std.json;
63 
64 import d.serialization;
65 
66 /**
67  * A UDA to mark a JSON field as required for deserialization
68  *
69  * When applied to a field, deserialization will throw if field is not found in json,
70  * and serialization will produce `null` for `null` fields
71  */
72 struct jsonRequired { }
73 
74 /**
75  * Serializes an object to a $(LREF JSONValue). To make this work, use $(LREF serializable) UDA on
76  * any fields that you want to be serializable. Automatically maps marked fields to
77  * corresponding JSON types. Any field not marked with $(LREF serializable) is not serialized.
78  */
79 JSONValue serializeJSON(T)(auto ref T obj) @safe
80 {
81     static if(isPointer!T)
82         return serializeJSON!(PointerTarget!T)(*obj);
83     else
84     {
85         JSONValue ret;
86         foreach(alias prop; readableSerializables!T)
87         {
88             enum name = getSerializableName!prop;
89             auto value = __traits(child, obj, prop);
90             static if(isArray!(typeof(prop)))
91             {
92                 if(value.length > 0)
93                     ret[name] = serializeAutoObj(value);
94                 else if(isJSONRequired!prop)
95                     ret[name] = JSONValue(null);
96             }
97             else ret[name] = serializeAutoObj(value);
98         }
99         return ret;
100     }
101 }
102 ///
103 @safe unittest
104 {
105     struct Test
106     {
107         @serializable int test = 43;
108         @serializable string other = "Hello, world";
109 
110         @serializable int foo() { return inaccessible; }
111         @serializable void foo(int val) { inaccessible = val; }
112     private:
113         int inaccessible = 32;
114     }
115 
116     auto val = serializeJSON(Test());
117     assert(val["test"].get!int == 43);
118     assert(val["other"].get!string == "Hello, world");
119     assert(val["foo"].get!int == 32);
120 }
121 
122 /**
123  * Serialize `T` into a JSON string
124  */
125 string serializeToJSONString(T)(auto ref T obj, in bool pretty = false) @safe
126 {
127     auto val = serializeJSON(obj);
128     return toJSON(val, pretty);
129 }
130 ///
131 @safe unittest
132 {
133     struct Test
134     {
135         @serializable int test = 43;
136         @serializable string other = "Hello, world";
137 
138         @serializable int foo() { return inaccessible; }
139         @serializable void foo(int val) { inaccessible = val; }
140     private:
141         int inaccessible = 32;
142     }
143 
144     assert(serializeToJSONString(Test()) == `{"foo":32,"other":"Hello, world","test":43}`);
145 }
146 /**
147  * Deserializes a $(LREF JSONValue) to `T`
148  *
149  * Throws: $(LREF SerializationException) if fails to create an instance of any class
150  *         $(LREF SerializationException) if a $(LREF jsonRequired) $(LREF serializable) is missing
151  */
152 T deserializeJSON(T)(auto ref JSONValue root) @safe
153 {
154     import std.stdio : writeln;
155     static if(is(T == class) || isPointer!T)
156     {
157         if(root.isNull)
158             return null;
159     }
160     T ret;
161     static if(is(T == class))
162     {
163         ret = new T();
164         if(ret is null)
165             throw new SerializationException("Could not create an instance of " ~ fullyQualifiedName!T);
166     }
167     foreach(alias prop; writeableSerializables!T)
168     {
169         enum name = getSerializableName!prop;
170         static if(isJSONRequired!prop)
171         {
172             if((name in root) is null && isJSONRequired!prop)
173                 throw new SerializationException("Missing required field \"" ~ name ~ "\" in JSON!");
174         }
175         if(name in root)
176         {
177             static if(isFunction!prop)
178                 __traits(child, ret, prop) = deserializeAutoObj!(Parameters!prop[0])(root[name]);
179             else
180                 __traits(child, ret, prop) = deserializeAutoObj!(typeof(prop))(root[name]);
181         }
182     }
183     return ret;
184 }
185 ///
186 @safe unittest
187 {
188     immutable json = `{"a": 123, "b": "Hello"}`;
189 
190     struct Test
191     {
192         @serializable int a;
193         @serializable string b;
194     }
195 
196     immutable test = deserializeJSON!Test(parseJSON(json));
197     assert(test.a == 123 && test.b == "Hello");
198 }
199 
200 /**
201  * Deserialize a JSON string into `T`
202  */
203 T deserializeJSONFromString(T)(string json) @safe
204 {
205     return deserializeJSON!T(parseJSON(json));
206 }
207 ///
208 @safe unittest
209 {
210     immutable json = `{"a": 123, "b": "Hello"}`;
211 
212     struct Test
213     {
214         @serializable int a;
215         @serializable string b;
216     }
217 
218     immutable test = deserializeJSONFromString!Test(json);
219     assert(test.a == 123 && test.b == "Hello");
220 }
221 
222 @safe unittest
223 {
224     immutable json = `{"a": 123}`;
225     struct A { @serializable("b") @jsonRequired int b; }
226     struct B { @serializable int a; }
227 
228     auto res = parseJSON(json);
229     assertThrown(deserializeJSON!A(res));
230     assertNotThrown(deserializeJSON!B(res));
231 }
232 
233 private @safe
234 {
235     JSONValue serializeAutoObj(T)(auto ref T obj) @trusted
236     {
237         static if(isJSONNumber!T || isJSONString!T || is(T == bool))
238             return JSONValue(obj);
239         else static if(is(T == struct))
240             return serializeJSON(obj);
241         else static if(is(T == class))
242             return serializeJSON(obj);
243         else static if(isPointer!T && is(PointerTarget!T == struct))
244             return obj is null ? JSONValue(null) : serializeJSON(obj);
245         else static if(isArray!T)
246             return serializeJSONArray(obj);
247         else static assert(false, "Cannot serialize type " ~ T.stringof);
248 
249     }
250 
251     JSONValue serializeJSONArray(T)(auto ref T obj) @trusted
252     {
253         JSONValue v = JSONValue(new JSONValue[0]);
254         foreach(i; obj)
255             v.array ~= serializeAutoObj(i);
256         return v;
257     }
258 
259     T deserializeAutoObj(T)(auto ref JSONValue value) @trusted
260     {
261         static if(is(T == struct))
262             return deserializeJSON!T(value);
263         else static if(isPointer!T && is(PointerTarget!T == struct))
264         {
265             if(value.isNull)
266                 return null;
267             alias underlying = PointerTarget!T;
268             underlying* ret = new underlying;
269             *ret = deserializeAutoObj!underlying(value);
270             return ret;
271         }
272         else static if(is(T == class))
273         {
274             return deserializeJSON!T(value);
275         }
276         else static if(isJSONString!T)
277             return value.get!T;
278         else static if(isArray!T)
279             return deserializeJSONArray!T(value);
280         else return value.get!T;
281     }
282 
283     T deserializeJSONArray(T)(auto ref JSONValue value) @trusted
284     {
285         T ret;
286         static if(!__traits(isStaticArray, T))
287             ret = new T(value.arrayNoRef.length);
288         foreach(i, val; value.arrayNoRef)
289             ret[i] = deserializeAutoObj!(typeof(ret[0]))(val);
290         return ret;
291     }
292 
293     template isJSONRequired(alias T)
294     {
295         enum bool isJSONRequired = getUDAs!(T, jsonRequired).length > 0;
296     }
297 
298     template isJSONNumber(T)
299     {
300         enum bool isJSONNumber = __traits(isScalar, T) && !isPointer!T && !is(T == bool);
301     }
302     ///
303     unittest
304     {
305         assert(isJSONNumber!int);
306         assert(isJSONNumber!float);
307         assert(!isJSONNumber!bool);
308         assert(!isJSONNumber!string);
309     }
310 
311     template isJSONString(T)
312     {
313         enum bool isJSONString = is(T == string) || is(T == wstring) || is(T == dstring);
314     }
315     ///
316     @safe unittest
317     {
318         assert(isJSONString!string && isJSONString!wstring && isJSONString!dstring);
319     }
320 }
321 // For UT purposes. Declaring those in a unittest causes frame pointer errors
322 version(unittest)
323 {
324     private struct TestStruct
325     {
326         @serializable int a;
327         @serializable string b;
328 
329         @serializable void foo(int val) @safe { inaccessible = val; }
330         @serializable int foo() @safe const { return inaccessible; }
331     private:
332         int inaccessible;
333     }
334 
335     private class Test
336     {
337         @serializable int a;
338         @serializable string b;
339     }
340 }
341 
342 // Test case for deserialization with getters
343 @safe unittest
344 {
345     string json = `{"a": 123, "b": "Hello", "foo": 345}`;
346     auto t = deserializeJSON!TestStruct(parseJSON(json));
347     assert(t.a == 123 && t.b == "Hello" && t.foo == 345);
348 }
349 
350 // Test case for deserializing classes
351 @safe unittest
352 {
353     string json = `{"a": 123, "b": "Hello"}`;
354     auto t = deserializeJSON!Test(parseJSON(json));
355     assert(t.a == 123 && t.b == "Hello");
356 }
357 
358 // Global unittest for everything
359 unittest
360 {
361     struct Other
362     {
363         @serializable
364         string name;
365 
366         @serializable
367         int id;
368     }
369 
370     static class TTT
371     {
372         @serializable string o = "o";
373     }
374 
375     struct Foo
376     {
377         // Works with or without brackets
378         @serializable int a = 123;
379         @serializable() double floating = 123;
380         @serializable int[3] arr = [1, 2, 3];
381         @serializable string name = "Hello";
382         @serializable("flag") bool check = true;
383         @serializable() Other object;
384         @serializable Other[3] arrayOfObjects;
385         @serializable Other* nullable = null;
386         @serializable Other* structField = new Other("t", 1);
387         @serializable Test classField = new Test();
388     }
389 
390     auto orig = Foo();
391     auto val = serializeJSON(Foo());
392     string res = toJSON(val);
393     auto back = deserializeJSON!Foo(parseJSON(res));
394     assert(back.a == orig.a);
395     assert(back.floating == orig.floating);
396     assert(back.structField.id == orig.structField.id);
397 }
398 
399 // Special tests to check compile-time messages
400 unittest
401 {
402     struct TooMany
403     {
404         @serializable @serializable int a;
405     }
406 
407     struct NotSetter
408     {
409         @serializable void b(int a, int b);
410     }
411 
412     TooMany a;
413     NotSetter b;
414 
415     assert(!__traits(compiles, serializeJSON(a))); // Error: Only 1 UDA is allowed per property
416     assert(!__traits(compiles, serializeJSON(b))); // Error: not a getter or a setter
417 }
418 
419 // Test for using return value
420 @safe unittest
421 {
422     struct A
423     {
424         @serializable int a;
425     }
426 
427     A a = deserializeJSON!A(parseJSON("{\"a\": 123}"));
428 }
429 
430 // Test for const and immutable objects
431 @safe unittest
432 {
433     struct A
434     {
435         @serializable int a = 12;
436     }
437 
438     static class B
439     {
440         @serializable int a = 12;
441     }
442 
443     struct C
444     {
445         @serializable int a() const { return _a; }
446         private int _a = 12;
447     }
448 
449     immutable aa = A();
450     const ab = A();
451 
452     immutable ba = new B();
453     immutable bb = new B();
454 
455     immutable ca = C();
456 
457     immutable expected = `{"a":12}`;
458 
459     assert(serializeToJSONString(aa) == expected);
460     assert(serializeToJSONString(ab) == expected);
461 
462     assert(serializeToJSONString(ba) == expected);
463     assert(serializeToJSONString(bb) == expected);
464 
465     assert(serializeToJSONString(C()) == expected);
466     assert(serializeToJSONString(ca) == expected);
467 }
468 
469 // Unittest for virtual getters
470 @safe unittest
471 {
472     static class A
473     {
474         @serializable int b() @safe { return 0; }
475     }
476 
477     static class B : A
478     {
479         override int b() @safe { return 1; }
480     }
481 
482     B b = new B();
483 
484     assert(serializeToJSONString(cast(A)b) == `{"b":1}`);
485 }