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 }