1 module tagion.hibon.HiBONJSON;
2 
3 @safe:
4 import std.conv : to;
5 import std.format;
6 import std.json;
7 import std.range.primitives : isInputRange;
8 import std.traits : EnumMembers, ForeachType, ReturnType, Unqual;
9 import std.base64;
10 
11 //import std.stdio;
12 
13 import tagion.basic.Message : message;
14 import tagion.hibon.BigNumber;
15 import tagion.hibon.Document : Document;
16 import tagion.hibon.HiBON : HiBON;
17 import tagion.hibon.HiBONBase;
18 import tagion.hibon.HiBONException;
19 import tagion.hibon.HiBONRecord : isHiBONRecord;
20 import tagion.hibon.HiBONtoText;
21 
22 // import tagion.utils.JSONOutStream;
23 // import tagion.utils.JSONInStream : JSONType;
24 
25 import tagion.basic.tagionexceptions : Check;
26 import tagion.utils.StdTime;
27 
28 /**
29  * Exception type used by tagion.hibon.HiBON module
30  */
31 class HiBON2JSONException : HiBONException {
32     this(string msg, string file = __FILE__, size_t line = __LINE__) pure {
33         super(msg, file, line);
34     }
35 }
36 
37 private alias check = Check!HiBON2JSONException;
38 
39 enum NotSupported = "none";
40 
41 protected Type[string] generateLabelMap(const(string[Type]) typemap) {
42     Type[string] result;
43     foreach (e, label; typemap) {
44         if (label != NotSupported) {
45             result[label] = e;
46         }
47     }
48     return result;
49 }
50 
51 enum typeMap = [
52         Type.NONE: NotSupported,
53         Type.VER: NotSupported,
54         Type.FLOAT32: "f32",
55         Type.FLOAT64: "f64",
56         Type.STRING: "$",
57         Type.BINARY: "*",
58         Type.DOCUMENT: "{}",
59         Type.BOOLEAN: "bool",
60         Type.TIME: "time",
61         Type.INT32: "i32",
62         Type.INT64: "i64",
63         Type.UINT32: "u32",
64         Type.UINT64: "u64",
65         Type.BIGINT: "big",
66 
67         Type.DEFINED_NATIVE: NotSupported,
68 
69         Type.DEFINED_ARRAY: NotSupported,
70         Type.NATIVE_DOCUMENT: NotSupported,
71         Type.NATIVE_HIBON_ARRAY: NotSupported,
72         Type.NATIVE_DOCUMENT_ARRAY: NotSupported,
73         Type.NATIVE_STRING_ARRAY: NotSupported
74     ];
75 
76 static unittest {
77     static foreach (E; EnumMembers!Type) {
78         assert(E in typeMap, format("TypeMap %s is not defined", E));
79     }
80 }
81 //    generateTypeMap;
82 enum labelMap = generateLabelMap(typeMap);
83 
84 enum {
85     TYPE = 0,
86     VALUE = 1,
87 }
88 
89 JSONValue toJSON(Document doc) {
90     return toJSONT!true(doc);
91 }
92 
93 JSONValue toJSON(T)(T value) if (isHiBONRecord!T) {
94     return toJSONT!true(value.toDoc);
95 }
96 
97 string toPretty(T)(T value) {
98     static if (is(T : const(HiBON))) {
99         const doc = Document(value);
100         return doc.toJSON.toPrettyString;
101     }
102     else {
103         return value.toJSON.toPrettyString;
104     }
105 }
106 
107 mixin template JSONString() {
108     import std.conv : to;
109     import std.format;
110 
111     void toString(scope void delegate(scope const(char)[]) @safe sink,
112     const FormatSpec!char fmt) const {
113         alias ThisT = typeof(this);
114         import tagion.basic.Types;
115         import tagion.hibon.Document;
116         import tagion.hibon.HiBON;
117         import tagion.hibon.HiBONJSON;
118         import tagion.hibon.HiBONRecord;
119 
120         static if (isHiBONRecord!ThisT) {
121             const doc = this.toDoc;
122         }
123         else static if (is(ThisT : const(Document))) {
124             const doc = this;
125         }
126         else static if (is(ThisT : const(HiBON))) {
127             const doc = Document(this);
128         }
129         else {
130             static assert(0, format("type %s is not supported for JSONString", ThisT.stringof));
131         }
132         switch (fmt.spec) {
133         case 'j':
134             // Normal stringefied JSON
135             sink(doc.toJSON.toString);
136             break;
137         case 'J':
138             // Normal stringefied JSON
139             sink(doc.toJSON.toPrettyString);
140             break;
141         case 's':
142             sink(doc.serialize.to!string);
143             break;
144         case '@':
145             sink(doc.serialize.encodeBase64);
146             break;
147         case 'x':
148             sink(format("%(%02x%)", doc.serialize));
149             break;
150         case 'X':
151             sink(format("%(%02x%)", doc.serialize));
152             break;
153         default:
154             throw new HiBON2JSONException("Unknown format specifier: %" ~ fmt.spec);
155         }
156     }
157 }
158 
159 struct toJSONT(bool HASHSAFE) {
160     @trusted static JSONValue opCall(const Document doc) {
161         JSONValue result;
162         immutable isarray = doc.isArray && !doc.empty;
163         if (isarray) {
164             result.array = null;
165             result.array.length = doc.length;
166         }
167         else {
168             result.object = null;
169         }
170         foreach (e; doc[]) {
171             with (Type) {
172             CaseType:
173                 switch (e.type) {
174                     static foreach (E; EnumMembers!Type) {
175                         static if (isHiBONBaseType(E)) {
176                 case E:
177                             static if (E is DOCUMENT) {
178                                 const sub_doc = e.by!E;
179                                 auto doc_element = toJSONT(sub_doc);
180                                 if (isarray) {
181                                     result.array[e.index] = JSONValue(doc_element);
182                                 }
183                                 else {
184                                     result[e.key] = doc_element;
185                                 }
186                             }
187                             else static if ((E is BOOLEAN) || (E is STRING)) {
188                                 if (isarray) {
189                                     result.array[e.index] = JSONValue(e.by!E);
190                                 }
191                                 else {
192                                     result[e.key] = JSONValue(e.by!E);
193                                 }
194                             }
195                             else {
196                                 auto doc_element = new JSONValue[2];
197                                 doc_element[TYPE] = JSONValue(typeMap[E]);
198                                 if (isarray) {
199                                     result.array[e.index] = toJSONType(e);
200                                 }
201                                 else {
202                                     result[e.key] = toJSONType(e);
203                                 }
204                             }
205                             break CaseType;
206                         }
207                     }
208                 default:
209 
210                     
211 
212                         .check(0, message("HiBON type %s not supported and can not be converted to JSON",
213                                 e.type));
214                 }
215             }
216         }
217         return result;
218     }
219 
220     static JSONValue[] toJSONType(Document.Element e) {
221         auto doc_element = new JSONValue[2];
222         doc_element[TYPE] = JSONValue(typeMap[e.type]);
223         with (Type) {
224         TypeCase:
225             switch (e.type) {
226                 static foreach (E; EnumMembers!Type) {
227             case E:
228                     static if (E is BOOLEAN) {
229                         doc_element[VALUE] = e.by!E;
230                     }
231                     else static if (E is INT32 || E is UINT32) {
232 
233                         doc_element[VALUE] = e.by!(E);
234                     }
235                     else static if (E is INT64 || E is UINT64) {
236                         doc_element[VALUE] = format("0x%x", e.by!(E));
237                     }
238                     else static if (E is BIGINT) {
239                         doc_element[VALUE] = encodeBase64(e.by!(E).serialize);
240                     }
241                     else static if (E is BINARY) {
242                         doc_element[VALUE] = encodeBase64(e.by!(E));
243                     }
244                     else static if (E is FLOAT32 || E is FLOAT64) {
245                         static if (HASHSAFE) {
246                             doc_element[VALUE] = format("%a", e.by!E);
247                         }
248                         else {
249                             doc_element[VALUE] = e.by!E;
250                         }
251                     }
252                     else static if (E is TIME) {
253                         import std.datetime;
254 
255                         SysTime sys_time = SysTime(cast(long) e.by!E);
256                         doc_element[VALUE] = sys_time.toISOExtString;
257                     }
258                     else {
259                         goto default;
260                     }
261                     break TypeCase;
262                 }
263             default:
264                 throw new HiBONException(format("Unsuported HiBON type %s", e.type));
265             }
266         }
267         return doc_element;
268     }
269 }
270 
271 HiBON toHiBON(scope const JSONValue json) {
272     static const(T) get(T)(scope JSONValue jvalue) {
273         alias UnqualT = Unqual!T;
274         static if (is(UnqualT == bool)) {
275             return jvalue.boolean;
276         }
277         else static if (is(UnqualT == uint)) {
278             long x = jvalue.integer;
279 
280             
281 
282             .check((x > 0) && (x <= uint.max), format("%s not a u32", jvalue));
283             return cast(uint) x;
284         }
285         else static if (is(UnqualT == int)) {
286             return jvalue.integer.to!int;
287         }
288         else static if (is(UnqualT == long) || is(UnqualT == ulong)) {
289             const text = jvalue.str;
290             ulong result;
291             if (isHexPrefix(text)) {
292                 result = text[hex_prefix.length .. $].to!ulong(16);
293             }
294             else {
295                 result = text.to!UnqualT;
296             }
297             static if (is(UnqualT == long)) {
298                 return cast(long) result;
299             }
300             else {
301                 return result;
302             }
303         }
304         else static if (is(UnqualT == string)) {
305             return jvalue.str;
306         }
307         else static if (is(T == immutable(ubyte)[])) {
308             return decode(jvalue.str);
309         }
310         else static if (is(T : const(double))) {
311             if (jvalue.type is JSONType.float_) {
312                 return jvalue.floating.to!UnqualT;
313             }
314             else {
315                 return jvalue.str.to!UnqualT;
316             }
317         }
318         else static if (is(T : U[], U)) {
319             scope array = new U[jvalue.array.length];
320             foreach (i, ref a; jvalue) {
321                 array[i] = a.get!U;
322             }
323             return array.idup;
324         }
325         else static if (is(T : const BigNumber)) {
326             const text = jvalue.str;
327             if (isBase64Prefix(text) || isHexPrefix(text)) {
328                 const data = decode(text);
329                 return BigNumber(data);
330             }
331             return BigNumber(jvalue.str);
332         }
333         else static if (is(T : const sdt_t)) {
334             import std.datetime;
335 
336             const text_time = get!string(jvalue);
337             const sys_time = SysTime.fromISOExtString(text_time);
338             return sdt_t(sys_time.stdTime);
339         }
340         else {
341             static assert(0, format("Type %s is not supported", T.stringof));
342         }
343         assert(0);
344     }
345 
346     static HiBON JSON(Key)(scope JSONValue json) {
347         static bool set(ref HiBON sub_result, Key key, scope JSONValue jvalue) {
348             if (jvalue.type is JSONType..string) {
349                 sub_result[key] = jvalue.str;
350                 return true;
351             }
352             else if ((jvalue.type is JSONType.true_) || (jvalue.type is JSONType.false_)) {
353                 sub_result[key] = jvalue.boolean;
354                 return true;
355             }
356             if ((jvalue.array.length != 2) || (jvalue.array[TYPE].type !is JSONType.STRING) || !(jvalue.array[TYPE].str in labelMap)) {
357                 return false;
358             }
359             immutable label = jvalue.array[TYPE].str;
360             immutable type = labelMap[label];
361 
362             with (Type) {
363                 final switch (type) {
364                     static foreach (E; EnumMembers!Type) {
365                 case E:
366                         static if (isHiBONBaseType(E)) {
367                             alias T = HiBON.Value.TypeT!E;
368                             scope value = jvalue.array[VALUE];
369 
370                             static if (E is DOCUMENT) {
371                                 return false;
372                             }
373                             else {
374                                 static if (E is BINARY) {
375                                     import std.uni : toLower;
376 
377                                     sub_result[key] = decode(value.str).idup;
378                                 }
379                                 else {
380                                     sub_result[key] = get!T(value);
381                                 }
382                                 return true;
383                             }
384                         }
385                         else {
386                             assert(0, format("Unsupported type %s for member %s", E, key));
387                         }
388                     }
389                 }
390             }
391 
392             assert(0);
393         }
394 
395         HiBON result = new HiBON;
396         foreach (Key key, ref jvalue; json) {
397             with (JSONType) {
398                 final switch (jvalue.type) {
399                 case null_:
400 
401                     
402 
403                         .check(0, "HiBON does not support null");
404                     break;
405                 case string:
406                     result[key] = jvalue.str;
407                     break;
408                 case integer:
409                     result[key] = jvalue.integer;
410                     break;
411                 case uinteger:
412                     result[key] = jvalue.uinteger;
413                     break;
414                 case float_:
415                     result[key] = jvalue.floating;
416                     break;
417                 case array:
418                     if (!set(result, key, jvalue)) {
419                         result[key] = Obj(jvalue);
420                     }
421                     break;
422                 case object:
423                     result[key] = Obj(jvalue);
424                     break;
425                 case true_:
426                     result[key] = true;
427                     break;
428                 case false_:
429                     result[key] = false;
430                     break;
431                 }
432             }
433         }
434         return result;
435     }
436 
437     @trusted static HiBON Obj(scope JSONValue json) {
438         if (json.type is JSONType.ARRAY) {
439             return JSON!size_t(json);
440         }
441         else if (json.type is JSONType.OBJECT) {
442             return JSON!string(json);
443         }
444 
445         
446 
447         .check(0, format("JSON_TYPE must be of %s or %s not %s",
448                 JSONType.OBJECT, JSONType.ARRAY, json.type));
449         assert(0);
450     }
451 
452     return Obj(json);
453 }
454 
455 HiBON toHiBON(const(char[]) json_text) {
456     const json = json_text.parseJSON;
457     return json.toHiBON;
458 }
459 
460 Document toDoc(scope const JSONValue json) {
461     return Document(json.toHiBON);
462 }
463 
464 Document toDoc(const(char[]) json_text) {
465     const json = parseJSON(json_text);
466     return json.toDoc;
467 }
468 
469 unittest {
470     //    import std.stdio;
471     import std.typecons : Tuple;
472     import tagion.hibon.HiBON : HiBON;
473 
474     alias Tabel = Tuple!(
475             float, Type.FLOAT32.stringof,
476             double, Type.FLOAT64.stringof,
477             bool, Type.BOOLEAN.stringof,
478             int, Type.INT32.stringof,
479             long, Type.INT64.stringof,
480             uint, Type.UINT32.stringof,
481             ulong, Type.UINT64.stringof,
482             BigNumber, Type.BIGINT.stringof,
483             sdt_t, Type.TIME.stringof);
484 
485     Tabel test_tabel;
486     test_tabel.FLOAT32 = 1.23;
487     test_tabel.FLOAT64 = 1.23e200;
488     test_tabel.INT32 = -42;
489     test_tabel.INT64 = -0x0123_3456_789A_BCDF;
490     test_tabel.UINT32 = 42;
491     test_tabel.UINT64 = 0x0123_3456_789A_BCDF;
492     test_tabel.BOOLEAN = true;
493     test_tabel.BIGINT = BigNumber("-1234_5678_9123_1234_5678_9123_1234_5678_9123");
494     test_tabel.TIME = sdt_t(1001);
495 
496     alias TabelArray = Tuple!(
497             immutable(ubyte)[], Type.BINARY.stringof,
498             string, Type.STRING.stringof,
499     );
500     TabelArray test_tabel_array;
501     test_tabel_array.BINARY = [1, 2, 3];
502     test_tabel_array.STRING = "Text";
503 
504     { // Empty Document
505         const doc = Document();
506         assert(doc.toJSON.toString == "{}");
507     }
508 
509     { // Test sample 1 HiBON Objects
510         auto hibon = new HiBON;
511         {
512             foreach (i, t; test_tabel) {
513                 enum name = test_tabel.fieldNames[i];
514                 hibon[name] = t;
515             }
516             auto sub_hibon = new HiBON;
517             hibon[sub_hibon.stringof] = sub_hibon;
518             foreach (i, t; test_tabel_array) {
519                 enum name = test_tabel_array.fieldNames[i];
520                 sub_hibon[name] = t;
521             }
522         }
523 
524         //
525         // Checks
526         // HiBON -> Document -> JSON -> HiBON -> Document
527         //
528         const doc = Document(hibon);
529 
530         pragma(msg, "fixme(cbr): For some unknown reason toString (mixin JSONString)",
531                 " is not @safe for Document and HiBON");
532 
533         assert(doc.toJSON.toPrettyString == doc.toPretty);
534         assert(doc.toJSON.toPrettyString == hibon.toPretty);
535     }
536 
537     { // Test sample 2 HiBON Array and Object
538         auto hibon = new HiBON;
539         {
540             foreach (i, t; test_tabel) {
541                 hibon[i] = t;
542             }
543             auto sub_hibon = new HiBON;
544             hibon[sub_hibon.stringof] = sub_hibon;
545             foreach (i, t; test_tabel_array) {
546                 sub_hibon[i] = t;
547             }
548         }
549 
550         //
551         // Checks
552         // HiBON -> Document -> JSON -> HiBON -> Document
553         //
554         const doc = Document(hibon);
555 
556         auto json = doc.toJSON;
557 
558         string str = json.toString;
559         auto parse = str.parseJSON;
560         auto h = parse.toHiBON;
561 
562         const parse_doc = Document(h.serialize);
563 
564         assert(doc == parse_doc);
565         assert(doc.toJSON.toString == parse_doc.toJSON.toString);
566     }
567 
568     { // Test sample 3 HiBON Array and Object
569         auto hibon = new HiBON;
570         {
571             foreach (i, t; test_tabel) {
572                 hibon[i] = t;
573             }
574             auto sub_hibon = new HiBON;
575             // Sub hibon is added to the last index of the hibon
576             // Which result keep hibon as an array
577             hibon[hibon.length] = sub_hibon;
578             foreach (i, t; test_tabel_array) {
579                 sub_hibon[i] = t;
580             }
581         }
582 
583         //
584         // Checks
585         // HiBON -> Document -> JSON -> HiBON -> Document
586         //
587         const doc = Document(hibon);
588 
589         auto json = doc.toJSON;
590 
591         string str = json.toString;
592         auto parse = str.parseJSON;
593         auto h = parse.toHiBON;
594 
595         const parse_doc = Document(h.serialize);
596 
597         assert(doc == parse_doc);
598         assert(doc.toJSON.toString == parse_doc.toJSON.toString);
599     }
600 }
601 
602 unittest {
603     import std.stdio;
604     import tagion.hibon.HiBONRecord;
605 
606     static struct S {
607         int[] a;
608         mixin HiBONRecord!(q{
609             this(int[] a) {
610                 this.a=a;
611             }
612          });
613     }
614 
615     { /// Checks that an array of two elements is converted correctly
616         const s = S([20, 34]);
617         immutable text = s.toPretty;
618         //const json = text.parseJSON;
619         const h = text.toHiBON;
620         const doc = Document(h);
621         const result_s = S(doc);
622         assert(result_s == s);
623     }
624 
625     { /// Checks 
626         const s = S([17, -20, 42]);
627         immutable text = s.toJSON;
628         const result_s = S(text.toDoc);
629         assert(result_s == s);
630 
631     }
632 
633 }