1 /******************************************************************************* 2 3 Serializer, to be used with the StructSerializer in 4 ocean.io.serialize.StructSerializer, which dumps a struct to a string. 5 6 Usage example (in conjunction with ocean.io.serialize.StructSerializer): 7 8 --- 9 10 // Example struct to serialize 11 struct Data 12 { 13 struct Id 14 { 15 cstring name; 16 hash_t id; 17 } 18 19 Id[] ids; 20 cstring name; 21 uint count; 22 float money; 23 } 24 25 // Set up some data in a struct 26 Data data; 27 test.ids = [Data.Id("hi", 23), Data.Id("hello", 17)]; 28 29 // Create serializer object 30 scope ser = new StringStructSerializer!(char)(); 31 32 // A string buffer 33 char[] output; 34 35 // Dump struct to buffer via serializer 36 ser.serialize(output, data); 37 38 --- 39 40 Copyright: 41 Copyright (c) 2009-2016 dunnhumby Germany GmbH. 42 All rights reserved. 43 44 License: 45 Boost Software License Version 1.0. See LICENSE_BOOST.txt for details. 46 Alternatively, this file may be distributed under the terms of the Tango 47 3-Clause BSD License (see LICENSE_BSD.txt for details). 48 49 *******************************************************************************/ 50 51 module ocean.io.serialize.StringStructSerializer; 52 53 54 55 56 import ocean.transition; 57 58 import ocean.core.Array; 59 60 import ocean.io.serialize.StructSerializer; 61 62 import ocean.text.convert.Formatter; 63 64 import ocean.text.util.Time; 65 66 import ocean.util.container.map.Set; 67 68 import ocean.core.Exception; 69 70 import ocean.meta.traits.Basic; 71 72 /******************************************************************************* 73 74 SerializerException 75 76 *******************************************************************************/ 77 78 class SerializerException : Exception 79 { 80 mixin ReusableExceptionImplementation!(); 81 } 82 83 /******************************************************************************* 84 85 Reusable exception instance. 86 87 *******************************************************************************/ 88 89 private SerializerException serializer_exception; 90 91 static this () 92 { 93 .serializer_exception = new SerializerException(); 94 } 95 96 97 /******************************************************************************* 98 99 String struct serializer 100 101 Params: 102 Char = character type of output string 103 104 *******************************************************************************/ 105 106 public class StringStructSerializer ( Char ) 107 { 108 static assert(isCharType!(Char), typeof(this).stringof ~ 109 " - this class can only handle {char, wchar, dchar}, not " ~ 110 Char.stringof); 111 112 113 /*************************************************************************** 114 115 Indentation size 116 117 ***************************************************************************/ 118 119 private static immutable indent_size = 3; 120 121 122 /*************************************************************************** 123 124 Indentation level string - filled with spaces. 125 126 ***************************************************************************/ 127 128 private Char[] indent; 129 130 131 /*************************************************************************** 132 133 format string for displaying an item of type floating point 134 135 ***************************************************************************/ 136 137 private cstring fp_format; 138 139 140 /*************************************************************************** 141 142 Known list of common timestamp field names 143 144 ***************************************************************************/ 145 146 private StandardHashingSet!(cstring) known_timestamp_fields; 147 148 149 /*************************************************************************** 150 151 Flag that is set to true if single character fields in structs should be 152 serialized into equivalent friendly string representations (applicable 153 only if these fields contain whitespace or other unprintable 154 characters). 155 e.g. the newline character will be serialized to the string '\n' instead 156 of to an actual new line. 157 158 ***************************************************************************/ 159 160 private bool turn_ws_char_to_str; 161 162 163 /*************************************************************************** 164 165 Temporary formatting buffer. 166 167 ***************************************************************************/ 168 169 private mstring buf; 170 171 172 /*************************************************************************** 173 174 Constructor, sets the maximum number of decimal digits to show for 175 floating point types. 176 177 Params: 178 fp_dec_to_display = maximum number of decimal digits to show for 179 floating point types. 180 181 ***************************************************************************/ 182 183 public this ( size_t fp_dec_to_display = 2 ) 184 { 185 mstring tmp = "{}{} {} : {:.".dup; 186 sformat(tmp, "{}", fp_dec_to_display); 187 tmp ~= "}\n"; 188 this.fp_format = tmp; 189 this.known_timestamp_fields = new StandardHashingSet!(cstring)(128); 190 } 191 192 193 /*************************************************************************** 194 195 Convenience method to serialize a struct. 196 197 If a field name of a struct matches one of the names in the 198 timestamp_fields array and implicitly converts to `ulong` 199 an ISO formatted string will be emitted in parentheses next to the 200 value of the field (which is assumed to be a unix timestamp). 201 202 Params: 203 T = type of item 204 output = string to serialize struct data to 205 item = item to append 206 timestamp_fields = (optional) an array of timestamp field names 207 turn_ws_char_to_str = true if individual whitespace or unprintable 208 character fields should be serialized into a friendlier string 209 representation, e.g. tab character into '\t' (defaults to false) 210 211 ***************************************************************************/ 212 213 public void serialize ( T ) ( ref Char[] output, ref T item, 214 cstring[] timestamp_fields = null, bool turn_ws_char_to_str = false ) 215 { 216 this.turn_ws_char_to_str = turn_ws_char_to_str; 217 218 this.known_timestamp_fields.clear(); 219 220 foreach (field_name; timestamp_fields) 221 { 222 this.known_timestamp_fields.put(field_name); 223 } 224 225 StructSerializer!(true).serialize(&item, this, output); 226 } 227 228 229 /*************************************************************************** 230 231 Called at the start of struct serialization - outputs the name of the 232 top-level object. 233 234 Params: 235 output = string to serialize struct data to 236 name = name of top-level object 237 238 ***************************************************************************/ 239 240 public void open ( ref Char[] output, cstring name ) 241 { 242 .serializer_exception.enforce(this.indent.length == 0, 243 "Non-zero indentation in open"); 244 245 sformat(output, "struct {}:\n", name); 246 this.increaseIndent(); 247 } 248 249 250 /*************************************************************************** 251 252 Called at the end of struct serialization 253 254 Params: 255 output = string to serialize struct data to 256 name = name of top-level object 257 258 ***************************************************************************/ 259 260 public void close ( ref Char[] output, cstring name ) 261 { 262 this.decreaseIndent(); 263 } 264 265 266 /*************************************************************************** 267 268 Appends a named item to the output string. 269 270 Note: the main method to use from the outside is the first serialize() 271 method above. This method is for the use of the StructSerializer. 272 273 Params: 274 T = type of item 275 output = string to serialize struct data to 276 item = item to append 277 name = name of item 278 279 ***************************************************************************/ 280 281 public void serialize ( T ) ( ref Char[] output, ref T item, cstring name ) 282 { 283 .serializer_exception.enforce(this.indent.length > 0, 284 "Incorrect indentation in serialize"); 285 286 // TODO: temporary support for unions by casting them to ubyte[] 287 static if ( is(T == union) ) 288 { 289 sformat(output, "{}union {} {} : {}\n", this.indent, T.stringof, 290 name, (cast(ubyte*)&item)[0..item.sizeof]); 291 } 292 else static if ( isFloatingPointType!(T) ) 293 { 294 sformat(output, this.fp_format, this.indent, T.stringof, name, 295 item); 296 } 297 else static if ( is(T == char) ) 298 { 299 // Individual character fields are handled in a special manner so 300 // that friendly string representations can be generated for them if 301 // necessary 302 303 sformat(output, "{}{} {} : {}\n", this.indent, T.stringof, name, 304 this.getCharAsString(item)); 305 } 306 else 307 { 308 sformat(output, "{}{} {} : {}", this.indent, T.stringof, name, 309 item); 310 311 if ( is(T : ulong) && name in this.known_timestamp_fields ) 312 { 313 Char[20] tmp; 314 sformat(output, " ({})\n", formatTime(item, tmp)); 315 } 316 else 317 { 318 sformat(output, "\n"); 319 } 320 } 321 } 322 323 324 /*************************************************************************** 325 326 Called before a sub-struct is serialized. 327 328 Params: 329 output = string to serialize struct data to 330 name = name of struct item 331 332 ***************************************************************************/ 333 334 public void openStruct ( ref Char[] output, cstring name ) 335 { 336 .serializer_exception.enforce(this.indent.length > 0, 337 "Incorrect indentation in openStruct"); 338 339 sformat(output, "{}struct {}:\n", this.indent, name); 340 this.increaseIndent(); 341 } 342 343 344 /*************************************************************************** 345 346 Called after a sub-struct is serialized. 347 348 Params: 349 output = string to serialize struct data to 350 name = name of struct item 351 352 ***************************************************************************/ 353 354 public void closeStruct ( ref Char[] output, cstring name ) 355 { 356 this.decreaseIndent(); 357 } 358 359 360 /*************************************************************************** 361 362 Appends a named array to the output string 363 364 Params: 365 T = base type of array 366 output = string to serialize struct data to 367 array = array to append 368 name = name of array item 369 370 ***************************************************************************/ 371 372 public void serializeArray ( T ) ( ref Char[] output, cstring name, 373 T[] array ) 374 { 375 .serializer_exception.enforce(this.indent.length > 0, 376 "Incorrect indentation in serializeArray"); 377 378 sformat(output, "{}{}[] {} (length {}):", this.indent, T.stringof, name, 379 array.length); 380 381 if ( array.length ) 382 { 383 sformat(output, " {}", array); 384 } 385 else 386 { 387 static if ( isCharType!(T) ) 388 { 389 sformat(output, ` ""`); 390 } 391 else 392 { 393 sformat(output, " []"); 394 } 395 } 396 397 sformat(output, "\n"); 398 } 399 400 401 /*************************************************************************** 402 403 Called before a struct array is serialized. 404 405 Params: 406 T = base type of array 407 output = string to serialize struct data to 408 name = name of struct item 409 array = array to append 410 411 ***************************************************************************/ 412 413 public void openStructArray ( T ) ( ref Char[] output, cstring name, 414 T[] array ) 415 { 416 .serializer_exception.enforce(this.indent.length > 0, 417 "Incorrect indentation in openStructArray"); 418 419 sformat(output, "{}{}[] {} (length {}):\n", this.indent, T.stringof, 420 name, array.length); 421 this.increaseIndent(); 422 } 423 424 425 /*************************************************************************** 426 427 Called after a struct array is serialized. 428 429 Params: 430 T = base type of array 431 output = string to serialize struct data to 432 name = name of struct item 433 array = array to append 434 435 ***************************************************************************/ 436 437 public void closeStructArray ( T ) ( ref Char[] output, cstring name, 438 T[] array ) 439 { 440 this.decreaseIndent(); 441 } 442 443 444 /*************************************************************************** 445 446 Increases the indentation level. 447 448 ***************************************************************************/ 449 450 private void increaseIndent ( ) 451 { 452 this.indent.length = this.indent.length + indent_size; 453 enableStomping(this.indent); 454 this.indent[] = ' '; 455 } 456 457 458 /*************************************************************************** 459 460 Decreases the indentation level. 461 462 ***************************************************************************/ 463 464 private void decreaseIndent ( ) 465 { 466 .serializer_exception.enforce(this.indent.length >= indent_size, 467 typeof(this).stringof ~ ".decreaseIndent - indentation cannot be decreased"); 468 469 this.indent.length = this.indent.length - indent_size; 470 enableStomping(this.indent); 471 } 472 473 474 /*************************************************************************** 475 476 Gets the string equivalent of a character. For most characters, the 477 string contains just the character itself; but in case of whitespace or 478 other unprintable characters, a friendlier string representation is 479 generated (provided the flag requesting this generation has been set). 480 For example, the string '\n' will be generated for the newline 481 character, '\t' for the tab character and so on. 482 483 Params: 484 c = character whose string equivalent is to be got 485 486 Returns: 487 string equivalent of the character 488 489 ***************************************************************************/ 490 491 private mstring getCharAsString ( char c ) 492 { 493 this.buf.length = 0; 494 enableStomping(this.buf); 495 496 if ( !this.turn_ws_char_to_str ) 497 { 498 sformat(this.buf, "{}", c); 499 return this.buf; 500 } 501 502 // The set of characters to use for creating cases within the following 503 // switch block. These are just whitepace or unprintable characters but 504 // without their preceding backslashes. 505 static immutable letters = ['0', 'a', 'b', 'f', 'n', 'r', 't', 'v']; 506 507 switch ( c ) 508 { 509 case c.init: 510 sformat(this.buf, "{}", "''"); 511 break; 512 513 mixin(ctfeCreateCases(letters)); 514 515 default: 516 sformat(this.buf, "{}", c); 517 break; 518 } 519 520 return this.buf; 521 } 522 523 524 /*************************************************************************** 525 526 Creates a string containing all the necessary case statements to be 527 mixed-in into the switch block that generates friendly string 528 representations of whitespace or unprintable characters. This function 529 is evaluated at compile-time. 530 531 Params: 532 letters = string containing all the characters corresponding to the 533 various case statements 534 535 Returns: 536 string containing all case statements to be mixed-in 537 538 ***************************************************************************/ 539 540 private static istring ctfeCreateCases ( istring letters ) 541 { 542 istring mixin_str; 543 544 foreach ( c; letters ) 545 { 546 mixin_str ~= 547 `case '\` ~ c ~ `':` ~ 548 `sformat(this.buf, "{}", "'\\` ~ c ~ `'");` ~ 549 `break;`; 550 } 551 552 return mixin_str; 553 } 554 } 555 556 version(UnitTest) 557 { 558 import ocean.core.Test; 559 import core.stdc.time; 560 } 561 562 unittest 563 { 564 // empty struct 565 566 auto serializer = new StringStructSerializer!(char); 567 mstring buffer; 568 569 struct EmptyStruct 570 { 571 } 572 573 EmptyStruct e; 574 575 serializer.serialize(buffer, e); 576 577 test!("==")(buffer.length, 20); 578 test!("==")(buffer, "struct EmptyStruct:\n"); 579 } 580 581 unittest 582 { 583 // regular arbitrary struct 584 585 auto serializer = new StringStructSerializer!(char); 586 mstring buffer; 587 588 struct TextFragment 589 { 590 char[] text; 591 int type; 592 } 593 594 TextFragment text_fragment; 595 text_fragment.text = "eins".dup; 596 text_fragment.type = 1; 597 598 serializer.serialize(buffer, text_fragment); 599 600 test!("==")(buffer.length, 69); 601 test!("==")(buffer, "struct TextFragment:\n" ~ 602 " char[] text (length 4): eins\n" ~ 603 " int type : 1\n"); 604 } 605 606 unittest 607 { 608 // struct with timestamp fields 609 610 auto serializer = new StringStructSerializer!(char); 611 mstring buffer; 612 cstring[] timestamp_fields = ["lastseen", "timestamp", "update_time"]; 613 614 struct TextFragmentTime 615 { 616 char[] text; 617 time_t time; // not detected 618 char[] lastseen; // not detected (doesn't convert to ulong) 619 time_t timestamp; // detected 620 time_t update_time; // detected 621 } 622 623 TextFragmentTime text_fragment_time; 624 text_fragment_time.text = "eins".dup; 625 text_fragment_time.time = 1456829726; 626 627 serializer.serialize(buffer, text_fragment_time, timestamp_fields); 628 629 test!("==")(buffer.length, 207); 630 test!("==")(buffer, "struct TextFragmentTime:\n" ~ 631 " char[] text (length 4): eins\n" ~ 632 " long time : 1456829726\n" ~ 633 ` char[] lastseen (length 0): ""` ~ "\n" ~ 634 " long timestamp : 0 (1970-01-01 00:00:00)\n" ~ 635 " long update_time : 0 (1970-01-01 00:00:00)\n"); 636 637 buffer.length = 0; 638 enableStomping(buffer); 639 serializer.serialize(buffer, text_fragment_time); 640 641 test!("==")(buffer.length, 163); 642 test!("==")(buffer, "struct TextFragmentTime:\n" ~ 643 " char[] text (length 4): eins\n" ~ 644 " long time : 1456829726\n" ~ 645 ` char[] lastseen (length 0): ""` ~ "\n" ~ 646 " long timestamp : 0\n" ~ 647 " long update_time : 0\n"); 648 } 649 650 unittest 651 { 652 // struct with multi-dimensional array field 653 654 auto serializer = new StringStructSerializer!(char); 655 mstring buffer; 656 657 struct TextFragment 658 { 659 char[] text; 660 int type; 661 } 662 663 struct MultiDimensionalArray 664 { 665 TextFragment[][] text_fragments; 666 } 667 668 MultiDimensionalArray multi_dimensional_array; 669 multi_dimensional_array.text_fragments ~= [[TextFragment("eins".dup, 1)], 670 [TextFragment("zwei".dup, 2), TextFragment("drei".dup, 3)]]; 671 672 serializer.serialize(buffer, multi_dimensional_array); 673 674 test!("==")(buffer.length, 461); 675 test!("==")(buffer, "struct MultiDimensionalArray:\n" ~ 676 " TextFragment[][] text_fragments (length 2):\n" ~ 677 " TextFragment[] text_fragments (length 1):\n" ~ 678 " struct TextFragment:\n" ~ 679 " char[] text (length 4): eins\n" ~ 680 " int type : 1\n" ~ 681 " TextFragment[] text_fragments (length 2):\n" ~ 682 " struct TextFragment:\n" ~ 683 " char[] text (length 4): zwei\n" ~ 684 " int type : 2\n" ~ 685 " struct TextFragment:\n" ~ 686 " char[] text (length 4): drei\n" ~ 687 " int type : 3\n"); 688 } 689 690 unittest 691 { 692 // struct with nested struct field 693 694 auto serializer = new StringStructSerializer!(char); 695 mstring buffer; 696 697 struct OuterStruct 698 { 699 int outer_a; 700 struct InnerStruct 701 { 702 int inner_a; 703 } 704 InnerStruct s; 705 } 706 707 OuterStruct s; 708 s.outer_a = 100; 709 s.s.inner_a = 200; 710 711 serializer.serialize(buffer, s); 712 713 test!("==")(buffer.length, 78); 714 test!("==")(buffer, "struct OuterStruct:\n" ~ 715 " int outer_a : 100\n" ~ 716 " struct s:\n" ~ 717 " int inner_a : 200\n"); 718 } 719 720 unittest 721 { 722 // struct with floating point fields 723 724 auto serializer = new StringStructSerializer!(char); 725 mstring buffer; 726 727 struct StructWithFloatingPoints 728 { 729 float a; 730 double b; 731 real c; 732 } 733 734 StructWithFloatingPoints sf; 735 sf.a = 10.00; 736 sf.b = 23.42; 737 738 serializer.serialize(buffer, sf); 739 740 test!("==")(buffer.length, 85); 741 test!("==")(buffer, "struct StructWithFloatingPoints:\n" ~ 742 " float a : 10\n" ~ 743 " double b : 23.42\n" ~ 744 " real c : nan\n"); 745 } 746 747 unittest 748 { 749 // struct with nested union field 750 751 auto serializer = new StringStructSerializer!(char); 752 mstring buffer; 753 754 struct StructWithUnion 755 { 756 union U 757 { 758 int a; 759 char b; 760 double c; 761 } 762 763 U u; 764 } 765 766 StructWithUnion su; 767 su.u.a = 100; 768 769 serializer.serialize(buffer, su); 770 771 test!("==")(buffer.length, 66); 772 test!("==")(buffer, "struct StructWithUnion:\n" ~ 773 " union U u : [100, 0, 0, 0, 0, 0, 0, 0]\n"); 774 775 su.u.b = 'a'; 776 777 buffer.length = 0; 778 enableStomping(buffer); 779 serializer.serialize(buffer, su); 780 781 test!("==")(buffer.length, 65); 782 test!("==")(buffer, "struct StructWithUnion:\n" ~ 783 " union U u : [97, 0, 0, 0, 0, 0, 0, 0]\n"); 784 } 785 786 unittest 787 { 788 // struct with individual char fields 789 790 auto serializer = new StringStructSerializer!(char); 791 mstring buffer; 792 793 struct StructWithChars 794 { 795 char c0; 796 char c1; 797 char c2; 798 char c3; 799 char c4; 800 char c5; 801 char c6; 802 char c7; 803 char c8; 804 char c9; 805 } 806 807 StructWithChars sc; 808 sc.c0 = 'g'; 809 sc.c1 = 'k'; 810 sc.c2 = '\0'; 811 sc.c3 = '\a'; 812 sc.c4 = '\b'; 813 sc.c5 = '\f'; 814 sc.c6 = '\n'; 815 sc.c7 = '\r'; 816 sc.c8 = '\t'; 817 sc.c9 = '\v'; 818 819 // Generation of friendly string representations of characters disabled 820 serializer.serialize(buffer, sc); 821 822 test!("==")(buffer.length, 174); 823 test!("==")(buffer, "struct StructWithChars:\n" ~ 824 " char c0 : g\n" ~ 825 " char c1 : k\n" ~ 826 " char c2 : \0\n" ~ 827 " char c3 : \a\n" ~ 828 " char c4 : \b\n" ~ 829 " char c5 : \f\n" ~ 830 " char c6 : \n\n" ~ 831 " char c7 : \r\n" ~ 832 " char c8 : \t\n" ~ 833 " char c9 : \v\n"); 834 835 // Generation of friendly string representations of characters enabled 836 buffer.length = 0; 837 enableStomping(buffer); 838 serializer.serialize(buffer, sc, [""], true); 839 840 test!("==")(buffer.length, 198); 841 test!("==")(buffer, "struct StructWithChars:\n" ~ 842 " char c0 : g\n" ~ 843 " char c1 : k\n" ~ 844 " char c2 : '\\0'\n" ~ 845 " char c3 : '\\a'\n" ~ 846 " char c4 : '\\b'\n" ~ 847 " char c5 : '\\f'\n" ~ 848 " char c6 : '\\n'\n" ~ 849 " char c7 : '\\r'\n" ~ 850 " char c8 : '\\t'\n" ~ 851 " char c9 : '\\v'\n"); 852 } 853 854 unittest 855 { 856 // struct with regular int arrays 857 858 auto serializer = new StringStructSerializer!(char); 859 mstring buffer; 860 861 struct StructWithIntArrays 862 { 863 int[] a; 864 int[] b; 865 } 866 867 StructWithIntArrays sia; 868 sia.a = [10, 20, 30]; 869 870 serializer.serialize(buffer, sia); 871 872 test!("==")(buffer.length, 90); 873 test!("==")(buffer, "struct StructWithIntArrays:\n" ~ 874 " int[] a (length 3): [10, 20, 30]\n" ~ 875 " int[] b (length 0): []\n"); 876 } 877 878 unittest 879 { 880 // struct with individual typedef field 881 882 auto serializer = new StringStructSerializer!(char); 883 mstring buffer; 884 885 mixin(Typedef!(hash_t, "AdskilletId")); 886 887 struct StructWithTypedef 888 { 889 AdskilletId a; 890 } 891 892 StructWithTypedef st; 893 st.a = cast(AdskilletId)1000; 894 895 serializer.serialize(buffer, st); 896 897 test!("==")(buffer.length, 50); 898 test!("==")(buffer, "struct StructWithTypedef:\n" ~ 899 " AdskilletId a : 1000\n"); 900 } 901 902 unittest 903 { 904 // struct with array of typedefs 905 906 auto serializer = new StringStructSerializer!(char); 907 mstring buffer; 908 909 mixin(Typedef!(hash_t, "AdskilletId")); 910 911 struct StructWithTypedefArray 912 { 913 AdskilletId[] ids; 914 } 915 916 StructWithTypedefArray sta; 917 918 sta.ids = new AdskilletId[](4); 919 920 foreach (idx, ref element; sta.ids) 921 { 922 element = cast(AdskilletId)(64 + idx); 923 } 924 925 serializer.serialize(buffer, sta); 926 927 test!("==")(buffer, "struct StructWithTypedefArray:\n" ~ 928 " AdskilletId[] ids (length 4): [64, 65, 66, 67]\n"); 929 }