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