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 }