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 }