1 module tagion.utils.JSONCommon;
2 
3 import std.meta : AliasSeq;
4 import std.traits : hasMember;
5 import tagion.basic.tagionexceptions;
6 
7 /++
8  +/
9 @safe
10 class OptionException : TagionException {
11     this(string msg, string file = __FILE__, size_t line = __LINE__) pure nothrow {
12         super(msg, file, line);
13     }
14 }
15 
16 enum isJSONCommon(T) = is(T == struct) && hasMember!(T, "toJSON");
17 
18 /++
19  mixin for implememts a JSON interface for a struct
20  +/
21 mixin template JSONCommon() {
22     import tagion.basic.tagionexceptions : Check;
23     import tagion.utils.JSONCommon : OptionException;
24 
25     alias check = Check!OptionException;
26     import std.conv : to;
27     import std.format;
28     import JSON = std.json;
29     import std.meta : AliasSeq;
30     import std.range : ElementType;
31     import std.traits;
32     import tagion.basic.basic : assumeTrusted, basename, isOneOf;
33 
34     //    import std.traits : isArray;
35     alias ArrayElementTypes = AliasSeq!(bool, string, double, int);
36 
37     enum isSupportedArray(T) = isArray!T && isSupported!(ElementType!T);
38     enum isSupportedAssociativeArray(T) = isAssociativeArray!T && is(KeyType!T == string) && isSupported!(ForeachType!T);
39     enum isSupported(T) = isOneOf!(T, ArrayElementTypes) || isNumeric!T ||
40         isSupportedArray!T || isJSONCommon!T ||
41         isSupportedAssociativeArray!T;
42 
43     /++
44      Returns:
45      JSON of the struct
46      +/
47     JSON.JSONValue toJSON() const @safe {
48         JSON.JSONValue result;
49         auto get(type, T)(T val) {
50 
51             static if (is(type == enum)) {
52                 return val.to!string;
53             }
54             else static if (is(type : immutable(ubyte[]))) {
55                 return val.toHexString;
56             }
57             else static if (is(type == struct)) {
58                 return val.toJSON;
59             }
60             else {
61                 return val;
62             }
63         }
64 
65         foreach (i, m; this.tupleof) {
66             enum name = basename!(this.tupleof[i]);
67             alias type = typeof(m);
68             static if (is(type == struct)) {
69                 result[name] = m.toJSON;
70             }
71             else static if (isArray!type && isSupported!(ForeachType!type)) {
72                 alias ElemType = ForeachType!type;
73                 JSON.JSONValue[] array;
74                 foreach (ref m_element; m) {
75                     JSON.JSONValue val = get!ElemType(m_element);
76                     array ~= val;
77                 }
78                 result[name] = array; // ~= m_element.toJSON;
79             }
80             else static if (isSupportedAssociativeArray!type) {
81                 JSON.JSONValue obj;
82                 alias ElemType = ForeachType!type;
83                 foreach (key, m_element; m) {
84                     obj[key] = get!ElemType(m_element);
85                 }
86                 result[name] = obj;
87             }
88             else {
89                 result[name] = get!(type)(m);
90                 version (none)
91                     static if (is(type == enum)) {
92                         result[name] = m.to!string;
93                     }
94                     else static if (is(type : immutable(ubyte[]))) {
95                         result[name] = m.toHexString;
96                     }
97                     else {
98                         result[name] = m;
99                     }
100             }
101         }
102         return result;
103     }
104 
105     /++
106      Stringify the struct
107      Params:
108      pretty = if true the return string is prettified else the string returned is compact
109      +/
110     string stringify(bool pretty = true)() const @safe {
111         static if (pretty) {
112             return toJSON.toPrettyString;
113         }
114         else {
115             return toJSON.toString;
116         }
117     }
118 
119     /++
120      Intialize a struct from a JSON
121      Params:
122      json_value = JSON used
123      +/
124     void parse(ref JSON.JSONValue json_value) @safe {
125         static void set_array(T)(ref T m, ref JSON.JSONValue[] json_array, string name) @safe if (isSupportedArray!T) {
126             foreach (_json_value; json_array) {
127                 ElementType!T m_element;
128                 set(m_element, _json_value, name);
129                 m ~= m_element;
130             }
131         }
132 
133         static void set_hashmap(T)(ref T m, ref JSON.JSONValue[string] json_array, string name) @safe
134                 if (isSupportedAssociativeArray!T) {
135             alias ElemType = ForeachType!T;
136             foreach (key, json_value; json_array) {
137                 ElemType val;
138                 set(val, json_value, name);
139                 m[key] = val;
140             }
141 
142         }
143 
144         static bool set(T)(ref T m, ref JSON.JSONValue _json_value, string _name) @safe {
145 
146             static if (is(T == struct)) {
147                 m.parse(_json_value);
148             }
149             else static if (is(T == enum)) {
150                 if (_json_value.type is JSON.JSONType..string) {
151                     switch (_json_value.str) {
152                         foreach (E; EnumMembers!T) {
153                     case E.to!string:
154                             m = E;
155                             return false;
156                             //                            continue ParseLoop;
157                         }
158                     default:
159                         check(0, format("Illegal value of %s only %s supported not %s",
160                                 _name, [EnumMembers!T],
161                                 _json_value.str));
162                     }
163                 }
164                 else static if (isIntegral!(BuiltinTypeOf!T)) {
165                     if (_json_value.type is JSON.JSONType.integer) {
166                         const value = _json_value.integer;
167                         switch (value) {
168                             foreach (E; EnumMembers!T) {
169                         case E:
170                                 m = E;
171                                 return false;
172                                 //continue ParseLoop;
173                             }
174                         default:
175                             // Fail;
176 
177                         }
178                     }
179                     check(0, format("Illegal value of %s only %s supported not %s",
180                             _name,
181                             [EnumMembers!T],
182                             _json_value.uinteger));
183                 }
184                 check(0, format("Illegal value of %s", _name));
185             }
186             else static if (is(T == string)) {
187                 m = _json_value.str;
188             }
189             else static if (isIntegral!T || isFloatingPoint!T) {
190                 static if (isIntegral!T) {
191                     auto value = _json_value.integer;
192                     check((value >= T.min) && (value <= T.max), format("Value %d out of range for type %s of %s", value, T
193                             .stringof, m.stringof));
194                 }
195                 else {
196                     auto value = _json_value.floating;
197                 }
198                 m = cast(T) value;
199 
200             }
201             else static if (is(T == bool)) {
202                 check((_json_value.type == JSON.JSONType.true_) || (
203                         _json_value.type == JSON.JSONType.false_),
204                         format("Type %s expected for %s but the json type is %s", T.stringof, m.stringof, _json_value
205                         .type));
206                 m = _json_value.type == JSON.JSONType.true_;
207             }
208             else static if (isSupportedArray!T) {
209                 check(_json_value.type is JSON.JSONType.array,
210                         format("Type of member '%s' must be an %s", _name, JSON.JSONType.array));
211                 (() @trusted => set_array(m, _json_value.array, _name))();
212 
213             }
214             else static if (isSupportedAssociativeArray!T) {
215                 check(_json_value.type is JSON.JSONType.object,
216                         format("Type of member '%s' must be an %s", _name, JSON.JSONType.object));
217                 (() @trusted => set_hashmap(m, _json_value.object, _name))();
218 
219             }
220             else {
221                 check(0, format("Unsupported type %s for '%s' member", T.stringof, _name));
222             }
223             return true;
224         }
225 
226         ParseLoop: foreach (i, ref member; this.tupleof) {
227             enum name = basename!(this.tupleof[i]);
228             alias type = typeof(member);
229             //            static if (!is(type == struct) || !is(type == class)) {
230             static assert(is(type == struct) || is(type == enum) || isSupported!type,
231                     format("Unsupported type %s for '%s' member", type.stringof, name));
232 
233             if (!set(member, json_value[name], name)) {
234                 continue ParseLoop;
235             }
236 
237         }
238     }
239 
240 }
241 
242 static T load(T)(string config_file) if (__traits(hasMember, T, "load")) {
243     T result;
244     result.load(config_file);
245     return result;
246 }
247 
248 import std.conv;
249 import std.json : JSONType, JSONValue;
250 
251 JSONValue toJSONType(string str, JSONType type) @safe pure {
252     with (JSONType) final switch (type) {
253     case float_:
254         return JSONValue(str.to!float);
255     case integer:
256         return JSONValue(str.to!int);
257     case uinteger:
258         return JSONValue(str.to!uint);
259     case null_:
260         return JSONValue(null);
261     case object, array, string: //best guess
262         return JSONValue(str);
263     case false_, true_:
264         return JSONValue(str.to!bool);
265     }
266 }
267 
268 mixin template JSONConfig() {
269     import std.file;
270     import JSON = std.json;
271 
272     void parseJSON(string json_text) @safe {
273         auto json = JSON.parseJSON(json_text);
274         parse(json);
275     }
276 
277     void load(string config_file) @safe {
278         if (config_file.exists) {
279             auto json_text = readText(config_file);
280             parseJSON(json_text);
281         }
282         else {
283             save(config_file);
284         }
285     }
286 
287     void save(const string config_file) @safe const {
288         config_file.write(stringify);
289     }
290 }
291 
292 version (unittest) {
293     import std.exception : assertThrown;
294     import std.json : JSONException;
295     import tagion.basic.Types : FileExtension;
296     import basic = tagion.basic.basic;
297 
298     const(basic.FileNames) fileId(T)(string prefix = null) @safe {
299         return basic.fileId!T(FileExtension.json, prefix);
300     }
301 
302     private enum Color {
303         red,
304         green,
305         blue,
306     }
307 
308 }
309 
310 @safe
311 unittest {
312     static struct OptS {
313         bool _bool;
314         string _string;
315         //        double _double;
316         int _int;
317         uint _uint;
318         Color color;
319         mixin JSONCommon;
320         mixin JSONConfig;
321     }
322 
323     OptS opt;
324     { // Simple JSONCommon check
325         opt._bool = true;
326         opt._string = "text";
327         // opt._double=4.2;
328         opt._int = -42;
329         opt._uint = 42;
330         opt.color = Color.blue;
331 
332         immutable filename = fileId!OptS.fullpath;
333         opt.save(filename);
334         OptS opt_loaded;
335         opt_loaded.load(filename);
336         assert(opt == opt_loaded);
337     }
338     static struct OptMain {
339         OptS sub_opt;
340         int main_x;
341         mixin JSONCommon;
342         mixin JSONConfig;
343     }
344 
345     immutable main_filename = fileId!OptMain.fullpath;
346 
347     { // Common check with sub a sub-structure
348         OptMain opt_main;
349         opt_main.sub_opt = opt;
350         opt_main.main_x = 117;
351         opt_main.save(main_filename);
352         //FileExtension
353         OptMain opt_loaded;
354         opt_loaded.load(main_filename);
355         assert(opt_main == opt_loaded);
356     }
357 
358     { // Check for bad JSONCommon file
359         OptS opt_s;
360         //immutable bad_filename = fileId!OptMain("bad").fullpath;
361         assertThrown!JSONException(opt_s.load(main_filename));
362     }
363 
364 }
365 
366 @safe
367 unittest { // JSONCommon with array types
368     //    import std.stdio;
369     static struct OptArray(T) {
370         T[] list;
371         mixin JSONCommon;
372         mixin JSONConfig;
373     }
374 
375     //alias StdType=AliasSeq!(bool, int, string, Color);
376 
377     { // Check JSONCommon with array of booleans
378         alias OptA = OptArray!bool;
379         OptA opt;
380         opt.list = [true, false, false, true, false];
381         immutable filename = fileId!OptA.fullpath;
382 
383         opt.save(filename);
384 
385         OptA opt_loaded;
386         opt_loaded.load(filename);
387 
388         assert(opt_loaded == opt);
389     }
390 
391     { // Check JSONCommon with array of string
392         alias OptA = OptArray!string;
393         OptA opt;
394         opt.list = ["Hugo", "Borge", "Brian", "Johnny", "Sven Bendt"];
395         immutable filename = fileId!OptA.fullpath;
396 
397         opt.save(filename);
398 
399         OptA opt_loaded;
400         opt_loaded.load(filename);
401 
402         assert(opt_loaded == opt);
403     }
404 
405     { // Check JSONCommon with array of integers
406         alias OptA = OptArray!int;
407         OptA opt;
408         opt.list = [42, -16, 117];
409         immutable filename = fileId!OptA.fullpath;
410 
411         opt.save(filename);
412 
413         OptA opt_loaded;
414         opt_loaded.load(filename);
415 
416         assert(opt_loaded == opt);
417     }
418 
419     {
420         static struct OptSub {
421             string text;
422             mixin JSONCommon;
423         }
424 
425         alias OptA = OptArray!OptSub;
426         OptA opt;
427         opt.list = [OptSub("Hugo"), OptSub("Borge"), OptSub("Brian")];
428         immutable filename = fileId!OptA.fullpath;
429 
430         opt.save(filename);
431 
432         OptA opt_loaded;
433         opt_loaded.load(filename);
434 
435         assert(opt_loaded == opt);
436     }
437 }
438 
439 @safe
440 unittest { // Check of support for associative array
441     static struct S {
442         string[string] names;
443         mixin JSONCommon;
444     }
445 
446     S s;
447     s.names["Hugo"] = "big";
448     s.names["Borge"] = "small";
449 
450     auto json = s.toJSON;
451 
452     S s_result;
453     s_result.parse(json);
454     assert("Hugo" in s_result.names);
455     assert("Borge" in s_result.names);
456     assert(s_result.names["Hugo"] == "big");
457     assert(s_result.names["Borge"] == "small");
458 
459 }