1 /++ 2 $(H4 High level JSON serialization API) 3 4 Macros: 5 IONREF = $(REF_ALTTEXT $(TT $2), $2, mir, ion, $1)$(NBSP) 6 +/ 7 module mir.ser.json; 8 9 public import mir.serde; 10 import mir.ion.exception: IonException, IonMirException; 11 12 private static immutable jsonAnnotationExceptionMsg = "JSON can store exactly one annotation."; 13 version(D_Exceptions) private static immutable jsonAnnotationException = new IonException(jsonAnnotationExceptionMsg); 14 15 /++ 16 JSON serialization back-end 17 +/ 18 struct JsonSerializer(string sep, Appender) 19 { 20 import mir.bignum.decimal: Decimal; 21 import mir.bignum.integer: BigInt; 22 import mir.ion.type_code; 23 import mir.lob; 24 import mir.timestamp; 25 import std.traits: isNumeric; 26 27 private enum hasDIP1008 = __traits(compiles, ()@nogc {throw new Exception("");}); 28 29 /++ 30 JSON string buffer 31 +/ 32 Appender* appender; 33 34 /// Mutable value used to choose format specidied or user-defined serialization specializations 35 int serdeTarget = SerdeTarget.json; 36 private bool _annotation; 37 private size_t state; 38 39 static if(sep.length) 40 { 41 private size_t deep; 42 43 private void putSpace() scope 44 { 45 for(auto k = deep; k; k--) 46 { 47 static if(sep.length == 1) 48 { 49 appender.put(sep[0]); 50 } 51 else 52 { 53 appender.put(sep); 54 } 55 } 56 } 57 } 58 59 scope: 60 61 private void pushState(size_t state) 62 { 63 this.state = state; 64 } 65 66 private size_t popState() 67 { 68 auto ret = state; 69 state = 0; 70 return ret; 71 } 72 73 private void incState() 74 { 75 if(state++) 76 { 77 static if(sep.length) 78 { 79 appender.put(",\n"); 80 } 81 else 82 { 83 appender.put(','); 84 } 85 } 86 else 87 { 88 static if(sep.length) 89 { 90 appender.put('\n'); 91 } 92 } 93 } 94 95 private void putEscapedKey(scope const char[] key) 96 { 97 incState; 98 static if(sep.length) 99 { 100 putSpace; 101 } 102 appender.put('\"'); 103 appender.put(key); 104 static if(sep.length) 105 { 106 appender.put(`": `); 107 } 108 else 109 { 110 appender.put(`":`); 111 } 112 } 113 114 /// 115 size_t stringBegin() 116 { 117 appender.put('\"'); 118 return 0; 119 } 120 121 /++ 122 Puts string part. The implementation allows to split string unicode points. 123 +/ 124 void putStringPart(scope const(char)[] value) 125 { 126 import mir.format: printEscaped, EscapeFormat; 127 printEscaped!(char, EscapeFormat.json)(appender, value); 128 } 129 130 /// 131 void stringEnd(size_t) 132 { 133 appender.put('\"'); 134 } 135 136 /// 137 size_t structBegin(size_t length = size_t.max) 138 { 139 static if(sep.length) 140 { 141 deep++; 142 } 143 appender.put('{'); 144 return popState; 145 } 146 147 /// 148 void structEnd(size_t state) 149 { 150 static if(sep.length) 151 { 152 deep--; 153 if (this.state) 154 { 155 appender.put('\n'); 156 putSpace; 157 } 158 } 159 appender.put('}'); 160 pushState(state); 161 } 162 163 /// 164 size_t listBegin(size_t length = size_t.max) 165 { 166 static if(sep.length) 167 { 168 deep++; 169 } 170 appender.put('['); 171 return popState; 172 } 173 174 /// 175 void listEnd(size_t state) 176 { 177 static if(sep.length) 178 { 179 deep--; 180 if (this.state) 181 { 182 appender.put('\n'); 183 putSpace; 184 } 185 } 186 appender.put(']'); 187 pushState(state); 188 } 189 190 /// 191 alias sexpBegin = listBegin; 192 193 /// 194 alias sexpEnd = listEnd; 195 196 /// 197 void putSymbol(scope const char[] symbol) 198 { 199 putValue(symbol); 200 } 201 202 /// 203 void putAnnotation(scope const(char)[] annotation) 204 { 205 import mir.exception: toMutable; 206 if (_annotation) 207 { 208 static if (hasDIP1008) 209 throw new IonMirException(jsonAnnotationExceptionMsg, " The second annotation is '", annotation, "'"); 210 else 211 throw jsonAnnotationException.toMutable; 212 } 213 _annotation = true; 214 putKey(annotation); 215 } 216 217 /// 218 auto annotationsEnd(size_t state) 219 { 220 _annotation = false; 221 return state; 222 } 223 224 /// 225 alias annotationWrapperBegin = structBegin; 226 227 /// 228 void annotationWrapperEnd(size_t annotationsState, size_t state) 229 { 230 return structEnd(state); 231 } 232 233 /// 234 void nextTopLevelValue() 235 { 236 appender.put('\n'); 237 } 238 239 /// 240 void putCompiletimeKey(string key)() 241 { 242 import mir.algorithm.iteration: any; 243 static if (key.any!(c => c == '"' || c == '\\' || c < ' ')) 244 putKey(key); 245 else 246 putEscapedKey(key); 247 } 248 249 /// 250 void putKey(scope const char[] key) 251 { 252 import mir.format: printEscaped, EscapeFormat; 253 254 incState; 255 static if(sep.length) 256 { 257 putSpace; 258 } 259 appender.put('\"'); 260 printEscaped!(char, EscapeFormat.json)(appender, key); 261 static if(sep.length) 262 { 263 appender.put(`": `); 264 } 265 else 266 { 267 appender.put(`":`); 268 } 269 } 270 271 /// 272 void putValue(Num)(const Num value) 273 if (isNumeric!Num && !is(Num == enum)) 274 { 275 import mir.format: print; 276 import mir.internal.utility: isFloatingPoint; 277 278 static if (isFloatingPoint!Num) 279 { 280 import mir.math.common: fabs; 281 282 if (value.fabs < value.infinity) 283 print(appender, value); 284 else if (value == Num.infinity) 285 appender.put(`"+inf"`); 286 else if (value == -Num.infinity) 287 appender.put(`"-inf"`); 288 else 289 appender.put(`"nan"`); 290 } 291 else 292 print(appender, value); 293 } 294 295 /// 296 void putValue(size_t size)(auto ref const BigInt!size num) 297 { 298 num.toString(appender); 299 } 300 301 /// 302 void putValue(size_t size)(auto ref const Decimal!size num) 303 { 304 num.toString(appender); 305 } 306 307 /// 308 void putValue(typeof(null)) 309 { 310 appender.put("null"); 311 } 312 313 /// ditto 314 void putNull(IonTypeCode code) 315 { 316 appender.put(code.nullStringJsonAlternative); 317 } 318 319 /// 320 void putValue(bool b) 321 { 322 appender.put(b ? "true" : "false"); 323 } 324 325 /// 326 void putValue(scope const char[] value) 327 { 328 auto state = stringBegin; 329 putStringPart(value); 330 stringEnd(state); 331 } 332 333 /// 334 void putValue(scope Clob value) 335 { 336 import mir.format: printEscaped, EscapeFormat; 337 338 static if(sep.length) 339 appender.put(`{{ "`); 340 else 341 appender.put(`{{"`); 342 343 printEscaped!(char, EscapeFormat.ionClob)(appender, value.data); 344 345 static if(sep.length) 346 appender.put(`" }}`); 347 else 348 appender.put(`"}}`); 349 } 350 351 /// 352 void putValue(scope Blob value) 353 { 354 import mir.base64 : encodeBase64; 355 static if(sep.length) 356 appender.put("{{ "); 357 else 358 appender.put("{{"); 359 360 encodeBase64(value.data, appender); 361 362 static if(sep.length) 363 appender.put(" }}"); 364 else 365 appender.put("}}"); 366 } 367 368 /// 369 void putValue(Timestamp value) 370 { 371 appender.put('\"'); 372 value.toISOExtString(appender); 373 appender.put('\"'); 374 } 375 376 /// 377 void elemBegin() 378 { 379 incState; 380 static if(sep.length) 381 { 382 putSpace; 383 } 384 } 385 386 /// 387 alias sexpElemBegin = elemBegin; 388 } 389 390 /++ 391 JSON serialization function. 392 +/ 393 alias serializeJson = serializeJsonPretty!""; 394 395 /// 396 version(mir_ion_test) 397 unittest 398 { 399 struct S 400 { 401 string foo; 402 uint bar; 403 } 404 405 assert(serializeJson(S("str", 4)) == `{"foo":"str","bar":4}`); 406 } 407 408 version(mir_ion_test) 409 unittest 410 { 411 import mir.ser.json: serializeJson; 412 import mir.format: stringBuf; 413 import mir.small_string; 414 415 SmallString!8 smll = SmallString!8("ciaociao"); 416 auto buffer = stringBuf; 417 418 serializeJson(buffer, smll); 419 assert(buffer.data == `"ciaociao"`); 420 } 421 422 /// 423 version(mir_ion_test) 424 unittest 425 { 426 import mir.serde: serdeIgnoreDefault; 427 428 static struct Decor 429 { 430 int candles; // 0 431 float fluff = float.infinity; // inf 432 } 433 434 static struct Cake 435 { 436 @serdeIgnoreDefault 437 string name = "Chocolate Cake"; 438 int slices = 8; 439 float flavor = 1; 440 @serdeIgnoreDefault 441 Decor dec = Decor(20); // { 20, inf } 442 } 443 444 assert(Cake("Normal Cake").serializeJson == `{"name":"Normal Cake","slices":8,"flavor":1.0}`); 445 auto cake = Cake.init; 446 cake.dec = Decor.init; 447 assert(cake.serializeJson == `{"slices":8,"flavor":1.0,"dec":{"candles":0,"fluff":"+inf"}}`); 448 assert(cake.dec.serializeJson == `{"candles":0,"fluff":"+inf"}`); 449 450 static struct A 451 { 452 @serdeIgnoreDefault 453 string str = "Banana"; 454 int i = 1; 455 } 456 assert(A.init.serializeJson == `{"i":1}`); 457 458 static struct S 459 { 460 @serdeIgnoreDefault 461 A a; 462 } 463 assert(S.init.serializeJson == `{}`); 464 assert(S(A("Berry")).serializeJson == `{"a":{"str":"Berry","i":1}}`); 465 466 static struct D 467 { 468 S s; 469 } 470 assert(D.init.serializeJson == `{"s":{}}`); 471 assert(D(S(A("Berry"))).serializeJson == `{"s":{"a":{"str":"Berry","i":1}}}`); 472 assert(D(S(A(null, 0))).serializeJson == `{"s":{"a":{"str":"","i":0}}}`); 473 474 static struct F 475 { 476 D d; 477 } 478 assert(F.init.serializeJson == `{"d":{"s":{}}}`); 479 } 480 481 /// 482 version(mir_ion_test) 483 unittest 484 { 485 import mir.serde: serdeIgnoreIn; 486 487 static struct S 488 { 489 @serdeIgnoreIn 490 string s; 491 } 492 // assert(`{"s":"d"}`.deserializeJson!S.s == null, `{"s":"d"}`.deserializeJson!S.s); 493 assert(S("d").serializeJson == `{"s":"d"}`); 494 } 495 496 /// 497 version(mir_ion_test) 498 unittest 499 { 500 import mir.deser.json; 501 502 static struct S 503 { 504 @serdeIgnoreOut 505 string s; 506 } 507 assert(`{"s":"d"}`.deserializeJson!S.s == "d"); 508 assert(S("d").serializeJson == `{}`); 509 } 510 511 /// 512 version(mir_ion_test) 513 unittest 514 { 515 import mir.serde: serdeIgnoreOutIf; 516 517 static struct S 518 { 519 @serdeIgnoreOutIf!`a < 0` 520 int a; 521 } 522 523 assert(serializeJson(S(3)) == `{"a":3}`, serializeJson(S(3))); 524 assert(serializeJson(S(-3)) == `{}`); 525 } 526 527 /// 528 version(mir_ion_test) 529 unittest 530 { 531 import mir.rc.array; 532 auto ar = rcarray!int(1, 2, 4); 533 assert(ar.serializeJson == "[1,2,4]"); 534 } 535 536 /// 537 version(mir_ion_test) 538 unittest 539 { 540 import mir.deser.json; 541 import std.range; 542 import std.algorithm; 543 import std.conv; 544 import mir.test; 545 546 static struct S 547 { 548 @serdeTransformIn!"a += 2" 549 @serdeTransformOut!(a =>"str".repeat.take(a).joiner("_").to!string) 550 int a; 551 } 552 553 auto s = deserializeJson!S(`{"a":3}`); 554 s.a.should == 5; 555 assert(serializeJson(s) == `{"a":"str_str_str_str_str"}`); 556 } 557 558 /++ 559 JSON serialization for custom outputt range. 560 +/ 561 version(mir_ion_test) 562 @safe pure nothrow @nogc 563 unittest 564 { 565 import mir.format: stringBuf; 566 auto buffer = stringBuf; 567 static struct S { int a; } 568 serializeJson(buffer, S(4)); 569 assert(buffer.data == `{"a":4}`); 570 } 571 572 /++ 573 JSON serialization function with pretty formatting and custom output range. 574 +/ 575 template serializeJsonPretty(string sep = "\t") 576 { 577 import mir.primitives: isOutputRange; 578 /// 579 void serializeJsonPretty(Appender, V)(scope ref Appender appender, scope auto ref V value, int serdeTarget = SerdeTarget.json) 580 if (isOutputRange!(Appender, const(char)[]) && isOutputRange!(Appender, char)) 581 { 582 import mir.ser: serializeValue; 583 auto serializer = jsonSerializer!sep((()@trusted => &appender)(), serdeTarget); 584 serializeValue(serializer, value); 585 } 586 587 /++ 588 JSON serialization function with pretty formatting. 589 +/ 590 string serializeJsonPretty(V)(scope auto ref const V value, int serdeTarget = SerdeTarget.json) 591 { 592 import std.array: appender; 593 import mir.functional: forward; 594 595 auto app = appender!(char[]); 596 serializeJsonPretty(app, value, serdeTarget); 597 return (()@trusted => cast(string) app.data)(); 598 } 599 } 600 601 /// 602 version(mir_ion_test) 603 unittest 604 { 605 static struct S { int a; } 606 assert(S(4).serializeJsonPretty!" " == "{\n \"a\": 4\n}"); 607 } 608 609 /// 610 version(mir_ion_test) 611 @safe pure nothrow @nogc 612 unittest 613 { 614 import mir.format: stringBuf; 615 auto buffer = stringBuf; 616 static struct S { int a; } 617 serializeJsonPretty!" "(buffer, S(4)); 618 assert(buffer.data == "{\n \"a\": 4\n}"); 619 } 620 621 /++ 622 Creates JSON serialization back-end. 623 Use `sep` equal to `"\t"` or `" "` for pretty formatting. 624 +/ 625 template jsonSerializer(string sep = "") 626 { 627 /// 628 auto jsonSerializer(Appender)(return Appender* appender, int serdeTarget = SerdeTarget.json) 629 { 630 return JsonSerializer!(sep, Appender)(appender, serdeTarget); 631 } 632 } 633 634 /// 635 @safe pure nothrow @nogc unittest 636 { 637 import mir.format: stringBuf; 638 import mir.bignum.integer; 639 640 auto buffer = stringBuf; 641 auto ser = jsonSerializer((()@trusted=>&buffer)(), 3); 642 auto state0 = ser.structBegin; 643 644 ser.putKey("null"); 645 ser.putValue(null); 646 647 ser.putKey("array"); 648 auto state1 = ser.listBegin(); 649 ser.elemBegin; ser.putValue(null); 650 ser.elemBegin; ser.putValue(123); 651 ser.elemBegin; ser.putValue(12300000.123); 652 ser.elemBegin; ser.putValue("\t"); 653 ser.elemBegin; ser.putValue("\r"); 654 ser.elemBegin; ser.putValue("\n"); 655 ser.elemBegin; ser.putValue(BigInt!2(1234567890)); 656 ser.listEnd(state1); 657 658 ser.structEnd(state0); 659 660 assert(buffer.data == `{"null":null,"array":[null,123,1.2300000123e+7,"\t","\r","\n",1234567890]}`); 661 } 662 663 /// 664 version(mir_ion_test) 665 unittest 666 { 667 import std.array; 668 import mir.bignum.integer; 669 670 auto app = appender!string; 671 auto ser = jsonSerializer!" "(&app); 672 auto state0 = ser.structBegin; 673 674 ser.putKey("null"); 675 ser.putValue(null); 676 677 ser.putKey("array"); 678 auto state1 = ser.listBegin(); 679 ser.elemBegin; ser.putValue(null); 680 ser.elemBegin; ser.putValue(123); 681 ser.elemBegin; ser.putValue(12300000.123); 682 ser.elemBegin; ser.putValue("\t"); 683 ser.elemBegin; ser.putValue("\r"); 684 ser.elemBegin; ser.putValue("\n"); 685 ser.elemBegin; ser.putValue(BigInt!2("1234567890")); 686 ser.listEnd(state1); 687 688 ser.structEnd(state0); 689 690 assert(app.data == 691 `{ 692 "null": null, 693 "array": [ 694 null, 695 123, 696 1.2300000123e+7, 697 "\t", 698 "\r", 699 "\n", 700 1234567890 701 ] 702 }`, app.data); 703 }