1 module mir.toml.serializer;
2 
3 // debug = Pops;
4 
5 import mir.bignum.decimal : Decimal;
6 import mir.bignum.integer : BigInt;
7 import mir.bignum.low_level_view : BigIntView;
8 import mir.format : printZeroPad;
9 import mir.ion.exception;
10 import mir.ion.type_code : IonTypeCode;
11 import mir.lob : Blob, Clob;
12 import mir.primitives : isOutputRange;
13 import mir.timestamp : Timestamp;
14 
15 import std.algorithm : among, canFind;
16 import std.array : join, replace;
17 import std.ascii : hexDigits;
18 import std.bitmanip : bitfields;
19 import std.conv : text, to;
20 import std.math : isNaN;
21 import std.meta : AliasSeq;
22 import std.regex : Captures, ctRegex, matchFirst, replaceAll;
23 import std.typecons : isBitFlagEnum;
24 
25 ///
26 struct TOMLBeautyConfig
27 {
28 	/// Uses inline tables inside arrays, no nested struct indent, no spaces
29 	/// around equals signs, no number separators, arrays are single line
30 	static immutable TOMLBeautyConfig minify = {
31 		fullTablesInArrays: false,
32 		spaceAroundEquals: false,
33 		decimalThousandsSeparatorThreshold: 0,
34 		binaryOctetSeparator: false,
35 		arrayIndent: "",
36 	};
37 	/// Uses inline tables inside arrays, no nested struct indent, adds spaces
38 	/// around equals signs, no number separators, arrays are single line
39 	static immutable TOMLBeautyConfig none = {
40 		fullTablesInArrays: false,
41 		decimalThousandsSeparatorThreshold: 0,
42 		binaryOctetSeparator: false,
43 		arrayIndent: "",
44 	};
45 	/// Uses inline tables inside arrays, no nested struct indent, adds spaces
46 	/// around equals signs, enabled decimal thousands separator at 5 digits,
47 	/// enabled binary octet separators, arrays are single line
48 	static immutable TOMLBeautyConfig numbers = {
49 		fullTablesInArrays: false,
50 		arrayIndent: "",
51 	};
52 	/// Uses proper tables inside arrays, no nested struct indent, adds spaces
53 	/// around equals, no number separators, arrays are single line
54 	static immutable TOMLBeautyConfig nestedTables = {
55 		fullTablesInArrays: false,
56 		decimalThousandsSeparatorThreshold: 0,
57 		binaryOctetSeparator: false,
58 		arrayIndent: "",
59 	};
60 	/// Uses proper tables inside arrays, 2-space struct indent, adds spaces
61 	/// around equals, no number separators, arrays are single line
62 	static immutable TOMLBeautyConfig indentedTables = {
63 		structIndent: "  ",
64 		decimalThousandsSeparatorThreshold: 0,
65 		binaryOctetSeparator: false,
66 		arrayIndent: "",
67 	};
68 	/// Uses proper tables inside arrays, two space indent for nested structs,
69 	/// adds spaces around equals, enabled decimal thousands separator at 5
70 	/// digits, enabled binary octet separators, arrays are multi-line
71 	static immutable TOMLBeautyConfig full = {
72 		structIndent: "  "
73 	};
74 
75 	string structIndent = "";
76 	string arrayIndent = "  ";
77 	bool fullTablesInArrays = true;
78 	bool arrayTrailingComma; // TODO
79 	string multilineStringIndent = ""; // TODO
80 	bool spaceAroundEquals = true;
81 	/// After how many decimal places to start putting thousands separators (_)
82 	/// or 0 to disable.
83 	int decimalThousandsSeparatorThreshold = 5; // TODO
84 	bool binaryOctetSeparator = true; // TODO
85 	bool hexWordSeparator = false; // TODO
86 	bool endOfFileNewline = true;
87 }
88 
89 package enum SerializationFlag
90 {
91 	none = 0,
92 	inlineTable = 1 << 0,
93 	inlineArray = 1 << 1,
94 	literalString = 1 << 2, /// String with `'` as quotes
95 	multilineString = 1 << 3, /// String with `"""` as quotes
96 	multilineLiteralString = 1 << 4, /// String with `'''` as quotes
97 }
98 
99 private enum CurrentType
100 {
101 	root, /// Before any `[sections]`
102 	table, /// `[fieldName]` table
103 	inlineTable, /// `fieldName = {}`
104 	array, /// `fieldName = [...]`
105 	tableArray /// `[[fieldName]]` table
106 }
107 
108 private struct ParseInfo
109 {
110 	mixin(bitfields!(
111 		CurrentType, "type", 3,
112 		bool, "firstEntry", 1,
113 		bool, "closedScope", 1,
114 		uint, "", 3
115 	));
116 
117 @safe pure:
118 
119 	string toString() const scope
120 	{
121 		import std.conv : text;
122 	
123 		return text("ParseInfo(", type, ", firstEntry: ", firstEntry, ", closedScope: ", closedScope, ")");
124 	}
125 
126 	static ParseInfo root()
127 	{
128 		ParseInfo ret;
129 		ret.type = CurrentType.root;
130 		ret.firstEntry = true;
131 		return ret;
132 	}
133 
134 	static ParseInfo table()
135 	{
136 		ParseInfo ret;
137 		ret.type = CurrentType.table;
138 		ret.firstEntry = true;
139 		return ret;
140 	}
141 
142 	static ParseInfo inlineTable()
143 	{
144 		ParseInfo ret;
145 		ret.type = CurrentType.inlineTable;
146 		ret.firstEntry = true;
147 		return ret;
148 	}
149 
150 	static ParseInfo array()
151 	{
152 		ParseInfo ret;
153 		ret.type = CurrentType.array;
154 		ret.firstEntry = true;
155 		return ret;
156 	}
157 
158 	static ParseInfo tableArray()
159 	{
160 		ParseInfo ret;
161 		ret.type = CurrentType.tableArray;
162 		ret.firstEntry = true;
163 		return ret;
164 	}
165 }
166 
167 private struct ParseInfoStack
168 {
169 	ParseInfo[32] stack;
170 	ParseInfo[] buffer;
171 	size_t len;
172 	debug (Pops)
173 		string[] popHistory;
174 
175 @safe pure:
176 
177 	@disable this(this);
178 
179 	alias list this;
180 
181 	inout(ParseInfo[]) list() inout return scope
182 	{
183 		return buffer[0 .. len];
184 	}
185 
186 	private void ensureBuffer() scope @trusted
187 	{
188 		if (buffer is null)
189 			buffer = stack[1 .. $];
190 	}
191 
192 	ref ParseInfo current() scope return
193 	{
194 		ensureBuffer();
195 
196 		if (len > 0 && len <= buffer.length)
197 			return buffer[len - 1];
198 		else
199 			return stack[0];
200 	}
201 
202 scope:
203 
204 	debug (Pops)
205 	{
206 		void push(ParseInfo i, string file = __FILE__, size_t line = __LINE__)
207 		{
208 			import std.range : repeat;
209 
210 			popHistory ~= text("  ".repeat(len).join, "- push ", file, ":", line, " ", i);
211 			ensureBuffer();
212 
213 			if (len >= buffer.length)
214 				buffer.length *= 2;
215 			buffer[len++] = i;
216 		}
217 
218 		void pop(string file = __FILE__, size_t line = __LINE__)
219 		{
220 			import std.range : repeat;
221 
222 			assert(len > 0, "too many pops! stack history:\n" ~ popHistory.join("\n"));
223 			len--;
224 			popHistory ~= text("  ".repeat(len).join, "-  pop ", file, ":", line, " ", buffer[len]);
225 		}
226 	}
227 	else
228 	{
229 		void push(ParseInfo i)
230 		{
231 			ensureBuffer();
232 
233 			if (len >= buffer.length)
234 				buffer.length *= 2;
235 			buffer[len++] = i;
236 		}
237 
238 		void pop()
239 		{
240 			assert(len > 0);
241 			len--;
242 		}
243 	}
244 
245 	bool inInlineTable()
246 	{
247 		foreach (ParseInfo pi; list)
248 			if (pi.type == CurrentType.table)
249 				return true;
250 		return false;
251 	}
252 
253 	bool inArray()
254 	{
255 		foreach (ParseInfo pi; list)
256 			if (pi.type == CurrentType.array)
257 				return true;
258 		return false;
259 	}
260 }
261 
262 static assert(isBitFlagEnum!SerializationFlag);
263 
264 struct TOMLSerializer(Appender)
265 {
266 	TOMLBeautyConfig beautyConfig;
267 
268 	private SerializationFlag currentFlags;
269 	private bool expectComma, expectSection;
270 	private char[256] currentKeyBuffer;
271 	private const(char)[] currentFullKey;
272 	private size_t keyArrayEnd;
273 	private size_t currentTableArrayKeyEnd;
274 	private ParseInfoStack stack;
275 	private CurrentType lastClosedScope;
276 
277 	/++
278 	TOML string buffer
279 	+/
280 	Appender* appender;
281 
282 @safe scope:
283 	@disable this(this);
284 
285 	private const(char)[] currentKey()
286 	{
287 		if (keyArrayEnd)
288 			return currentFullKey[keyArrayEnd + 1 .. $];
289 		else
290 			return currentFullKey;
291 	}
292 
293 	package bool pushFlag(SerializationFlag f)
294 	{
295 		if (hasFlag(f))
296 			return false;
297 		currentFlags |= f;
298 		return true;
299 	}
300 
301 	package void removeFlag(SerializationFlag f)
302 	{
303 		currentFlags &= ~f;
304 	}
305 
306 	package bool hasFlag(SerializationFlag f)
307 	{
308 		return (currentFlags & f) == f;
309 	}
310 
311 	private void putStartOfLine() scope
312 	{
313 		bool hasStruct = beautyConfig.structIndent.length > 0;
314 		bool hasArray = beautyConfig.arrayIndent.length > 0;
315 		if (hasStruct || hasArray)
316 			foreach (state; stack)
317 			{
318 				if (hasStruct && state.type == CurrentType.table)
319 				{
320 					if (beautyConfig.structIndent.length == 1)
321 						appender.put(beautyConfig.structIndent[0]);
322 					else
323 						appender.put(beautyConfig.structIndent);
324 				}
325 				if (hasArray && state.type == CurrentType.array)
326 				{
327 					if (beautyConfig.arrayIndent.length == 1)
328 						appender.put(beautyConfig.arrayIndent[0]);
329 					else
330 						appender.put(beautyConfig.arrayIndent);
331 				}
332 			}
333 	}
334 
335 	///
336 	size_t stringBegin()
337 	{
338 		if (hasFlag(SerializationFlag.literalString))
339 			appender.put('\'');
340 		else if (hasFlag(SerializationFlag.multilineString))
341 			appender.put(`"""`);
342 		else if (hasFlag(SerializationFlag.multilineLiteralString))
343 			appender.put(`'''`);
344 		else
345 			appender.put('"');
346 		return 0;
347 	}
348 
349 	/++
350 	Puts string part. The implementation allows to split string unicode points.
351 	+/
352 	void putStringPart(scope const(char)[] value)
353 	{
354 		static string replaceChar(dchar c)
355 		{
356 			switch (c)
357 			{
358 			case '\x08':
359 				return `\b`;
360 			case '\x09':
361 				return `\t`;
362 			case '\x0a':
363 				return `\n`;
364 			case '\x0c':
365 				return `\f`;
366 			case '\x0d':
367 				return `\r`;
368 			case '\x22':
369 				return `\"`;
370 			case '\x5c':
371 				return `\\`;
372 			default:
373 				char[6] hex = '0';
374 				hex[0] = '\\';
375 				hex[1] = 'u';
376 				int pos = hex.length - 1;
377 				uint i = cast(uint)c;
378 				assert(i <= ushort.max);
379 				while (i > 0)
380 				{
381 					hex[pos--] = hexDigits[i % 16];
382 					i /= 16;
383 				}
384 				return hex[].idup;
385 			}
386 		}
387 
388 		static string replaceMatch(scope Captures!(const(char)[]) m)
389 		{
390 			assert(m.hit.length == 1);
391 			return replaceChar(m.hit[0]);
392 		}
393 
394 		// need escape: backslash and the control characters other than tab, line feed, and carriage return
395 		// (U+0000 to U+0008, U+000B, U+000C, U+000E to U+001F, U+007F).
396 		static immutable needEscape = ctRegex!`[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]`;
397 
398 		bool isLiteral;
399 		bool isMultiline;
400 		if (hasFlag(SerializationFlag.literalString) || hasFlag(SerializationFlag.multilineLiteralString))
401 			isLiteral = true;
402 		if (hasFlag(SerializationFlag.multilineString) || hasFlag(SerializationFlag.multilineLiteralString))
403 			isMultiline = true;
404 
405 		if (isLiteral && value.matchFirst(needEscape))
406 			throw new IonException("Tried to serialize string as literal containg "
407 				~ "a control character, which is not supported by this string type.");
408 		else
409 			value = value.replaceAll!replaceMatch(needEscape);
410 
411 		if (isLiteral && isMultiline)
412 		{
413 			if (value.canFind(`'''`))
414 				throw new IonException("Tried to serialize string as multiline literal containg "
415 					~ "`'''`, which is not supported by this string type.");
416 		}
417 		else if (isLiteral)
418 		{
419 			if (value.canFind(`'`))
420 				throw new IonException("Tried to serialize string as literal containg "
421 					~ "`'`, which is not supported by this string type.");
422 		}
423 		else if (isMultiline)
424 		{
425 			value = value.replace(`\`, `\\`).replace(`"""`, `""\"`);
426 		}
427 		else
428 		{
429 			value = value.replace(`\`, `\\`).replace(`"`, `\"`);
430 		}
431 
432 		appender.put(value);
433 	}
434 
435 	///
436 	void stringEnd(size_t state)
437 	{
438 		if (hasFlag(SerializationFlag.literalString))
439 			appender.put('\'');
440 		else if (hasFlag(SerializationFlag.multilineString))
441 			appender.put(`"""`);
442 		else if (hasFlag(SerializationFlag.multilineLiteralString))
443 			appender.put(`'''`);
444 		else
445 			appender.put('"');
446 	}
447 
448 	// ///
449 	// void putAnnotation(scope const(char)[] annotation)
450 	// {
451 	// }
452 
453 	// ///
454 	// size_t annotationsEnd(size_t state)
455 	// {
456 	// }
457 
458 	// ///
459 	// size_t annotationWrapperBegin()
460 	// {
461 	// }
462 
463 	// ///
464 	// void annotationWrapperEnd(size_t annotationsState, size_t state)
465 	// {
466 	// }
467 
468 	///
469 	size_t structBegin(size_t length = size_t.max)
470 	{
471 		if (stack.current.type == CurrentType.tableArray)
472 		{
473 			auto previousKey = keyArrayEnd;
474 			keyArrayEnd = currentFullKey.length;
475 			if (stack.current.firstEntry)
476 				appender.put('\n');
477 			else
478 				appender.put("\n\n");
479 			putStartOfLine();
480 			appender.put("[[");
481 			appender.put(currentFullKey);
482 			appender.put("]]");
483 			stack.push(ParseInfo.inlineTable);
484 			stack.current.firstEntry = false;
485 			lastClosedScope = CurrentType.root;
486 			return previousKey;
487 		}
488 		else if (hasFlag(SerializationFlag.inlineTable) || stack.current.type.among!(CurrentType.inlineTable, CurrentType.array))
489 		{
490 			putKeyImpl();
491 			appender.put('{');
492 			stack.push(ParseInfo.inlineTable);
493 			lastClosedScope = CurrentType.root;
494 			return keyArrayEnd;
495 		}
496 		else if (currentKey.length)
497 		{
498 			auto previousKey = keyArrayEnd;
499 			keyArrayEnd = currentFullKey.length;
500 			if (stack.current.firstEntry)
501 				appender.put('\n');
502 			else
503 				appender.put("\n\n");
504 			putStartOfLine();
505 			appender.put('[');
506 			appender.put(currentFullKey);
507 			appender.put(']');
508 			stack.current.firstEntry = false;
509 			stack.push(ParseInfo.table);
510 			lastClosedScope = CurrentType.root;
511 			return previousKey;
512 		}
513 		else
514 		{
515 			stack.push(ParseInfo.root);
516 			return keyArrayEnd;
517 		}
518 	}
519 
520 	///
521 	void structEnd(size_t state)
522 	{
523 		if (stack.current.type == CurrentType.inlineTable)
524 		{
525 			stack.pop();
526 			if (beautyConfig.spaceAroundEquals)
527 				appender.put(" }");
528 			else
529 				appender.put('}');
530 			lastClosedScope = CurrentType.inlineTable;
531 		}
532 		else
533 		{
534 			lastClosedScope = stack.current.type;
535 			if (stack.current.type == CurrentType.root && beautyConfig.endOfFileNewline)
536 				appender.put('\n'); // end of file
537 			stack.pop();
538 		}
539 		keyArrayEnd = state;
540 		dropKey();
541 	}
542 
543 	///
544 	size_t listBegin(size_t length = size_t.max)
545 	{
546 		if (hasFlag(SerializationFlag.inlineArray)
547 			|| stack.current.type.among!(CurrentType.inlineTable, CurrentType.array, CurrentType.tableArray)
548 			|| !beautyConfig.fullTablesInArrays)
549 		{
550 			putKeyImpl();
551 			appender.put('[');
552 			stack.push(ParseInfo.array);
553 			lastClosedScope = CurrentType.root;
554 			return keyArrayEnd;
555 		}
556 		else
557 		{
558 			auto previousKey = keyArrayEnd;
559 			keyArrayEnd = currentFullKey.length;
560 			stack.push(ParseInfo.tableArray);
561 			currentTableArrayKeyEnd = previousKey;
562 			lastClosedScope = CurrentType.root;
563 			return previousKey;
564 		}
565 	}
566 
567 	///
568 	void elemBegin()
569 	{
570 	}
571 
572 	///
573 	alias sexpElemBegin = elemBegin;
574 
575 	///
576 	void listEnd(size_t state)
577 	{
578 		if (stack.current.type == CurrentType.array)
579 		{
580 			stack.pop();
581 			if (beautyConfig.spaceAroundEquals)
582 				appender.put(" ]");
583 			else
584 				appender.put(']');
585 			lastClosedScope = CurrentType.root;
586 		}
587 		else if (stack.current.firstEntry)
588 		{
589 			lastClosedScope = CurrentType.root;
590 			keyArrayEnd = state;
591 			appender.put(currentKey);
592 			if (beautyConfig.spaceAroundEquals)
593 				appender.put(" = []");
594 			else
595 				appender.put("=[]");
596 		}
597 		else
598 		{
599 			lastClosedScope = stack.current.type;
600 			stack.pop();
601 			appender.put('\n');
602 		}
603 		keyArrayEnd = state;
604 		dropKey();
605 	}
606 
607 	///
608 	alias sexpBegin = listBegin;
609 
610 	///
611 	alias sexpEnd = listEnd;
612 
613 	///
614 	void nextTopLevelValue()
615 	{
616 	}
617 
618 	///
619 	void putKey(scope const char[] key) @trusted
620 	{
621 		if (currentFullKey.length)
622 		{
623 			if (currentFullKey.length + 1 + key.length < currentKeyBuffer.length
624 				&& currentFullKey.ptr is currentKeyBuffer.ptr)
625 			{
626 				currentKeyBuffer.ptr[currentFullKey.length] = '.';
627 				currentKeyBuffer.ptr[currentFullKey.length + 1 .. currentFullKey.length + 1 + key.length] = key;
628 				currentFullKey = currentKeyBuffer.ptr[0 .. currentFullKey.length + 1 + key.length];
629 			}
630 			else
631 			{
632 				currentFullKey = text(currentFullKey, ".", key);
633 			}
634 		}
635 		else
636 		{
637 			if (key.length < currentKeyBuffer.length)
638 			{
639 				currentKeyBuffer.ptr[0 .. key.length] = key;
640 				currentFullKey = currentKeyBuffer.ptr[0 .. key.length];
641 			}
642 			else
643 			{
644 				currentFullKey = key.idup;
645 			}
646 		}
647 	}
648 
649 	private void dropKey()
650 	{
651 		currentFullKey = currentFullKey[0 .. keyArrayEnd];
652 	}
653 
654 	private void putKeyImpl()
655 	{
656 		scope (exit)
657 			dropKey();
658 
659 		if (lastClosedScope != CurrentType.root)
660 		{
661 			if (lastClosedScope == CurrentType.array)
662 				throw new IonException("Attempted to put TOML key after end of array [current key = " ~ currentFullKey.idup ~ "]");
663 			else if (lastClosedScope == CurrentType.inlineTable)
664 				throw new IonException("Attempted to put TOML key after end of inline table [current key = " ~ currentFullKey.idup ~ "]");
665 			else if (lastClosedScope == CurrentType.table || lastClosedScope == CurrentType.tableArray)
666 				throw new IonException("Can't put any more TOML keys after a table or array of tables has ended! "
667 					~ "Move value fields inside serialized struct before structs and struct arrays! [current key = " ~ currentFullKey.idup ~ "]");
668 			else
669 				assert(false, "unexpected lastClosedScope state in TOMLSerializer [current key = " ~ currentFullKey.idup ~ "]");
670 		}
671 
672 		final switch (stack.current.type)
673 		{
674 			case CurrentType.tableArray:
675 				if (stack.current.firstEntry)
676 				{
677 					appender.put('\n');
678 					putStartOfLine();
679 					appender.put(currentFullKey[currentTableArrayKeyEnd + 1 .. keyArrayEnd]);
680 					keyArrayEnd = currentTableArrayKeyEnd;
681 					if (beautyConfig.spaceAroundEquals)
682 						appender.put(" = [");
683 					else
684 						appender.put("=[");
685 					stack.current.type = CurrentType.array;
686 					lastClosedScope = CurrentType.root;
687 					goto case CurrentType.array;
688 				}
689 				else
690 				{
691 					assert(false, "unexpected stack state in TOMLSerializer: currently in an array of tables, "
692 						~ "but trying to put a key instead of a table. Perhaps you tried to mix tables and values? "
693 						~ "[current key = " ~ currentFullKey.idup ~ "]");
694 				}
695 			case CurrentType.root:
696 			case CurrentType.table:
697 				if (stack.current.type == CurrentType.table || !stack.current.firstEntry)
698 					appender.put('\n');
699 				stack.current.firstEntry = false;
700 				putStartOfLine();
701 				appender.put(currentKey);
702 				if (beautyConfig.spaceAroundEquals)
703 					appender.put(" = ");
704 				else
705 					appender.put('=');
706 				break;
707 			case CurrentType.array:
708 				if (!stack.current.firstEntry)
709 					appender.put(',');
710 				stack.current.firstEntry = false;
711 				if (stack.inInlineTable || !beautyConfig.arrayIndent.length)
712 				{
713 					if (beautyConfig.spaceAroundEquals)
714 						appender.put(' ');
715 				}
716 				else
717 				{
718 					appender.put('\n');
719 					putStartOfLine();
720 				}
721 				break;
722 			case CurrentType.inlineTable:
723 				if (!stack.current.firstEntry)
724 					appender.put(',');
725 				stack.current.firstEntry = false;
726 				if (beautyConfig.spaceAroundEquals)
727 					appender.put(' ');
728 				appender.put(currentKey);
729 				if (beautyConfig.spaceAroundEquals)
730 					appender.put(" = ");
731 				else
732 					appender.put('=');
733 				break;
734 		}
735 	}
736 
737 	///
738 	static foreach (T; AliasSeq!(byte, ubyte, short, ushort, int, uint, long, ulong))
739 		void putValue(T value)
740 		{
741 			putKeyImpl();
742 			appender.put(value.to!string);
743 		}
744 
745 	///
746 	void putValue(scope ref const BigInt!128 value)
747 	{
748 		putKeyImpl();
749 		appender.put(value.toString);
750 	}
751 
752 	///
753 	static foreach (T; AliasSeq!(float, double, real))
754 		void putValue(T value)
755 		{
756 			putKeyImpl();
757 			if (isNaN(value))
758 				appender.put("nan");
759 			else if (value == T.infinity)
760 				appender.put("inf");
761 			else if (value == -T.infinity)
762 				appender.put("-inf");
763 			else
764 				appender.put(value.to!string);
765 		}
766 
767 	///
768 	void putValue(scope ref const Decimal!128 value)
769 	{
770 		putKeyImpl();
771 		value.toString(*appender);
772 	}
773 
774 	///
775 	void putValue(typeof(null))
776 	{
777 		if (stack.current.type.among!(CurrentType.array, CurrentType.inlineTable))
778 		{
779 			throw new IonException("Tried to serialize null value inside array or inlineTable, which is forbidden.");
780 		}
781 		else
782 		{
783 			// ignore null values, no representation in TOML
784 			dropKey();
785 		}
786 	}
787 
788 	/// ditto 
789 	void putNull(IonTypeCode code)
790 	{
791 		switch (code)
792 		{
793 		case IonTypeCode.list:
794 			putKeyImpl();
795 			appender.put("[]");
796 			break;
797 		default:
798 			putValue(null);
799 			break;
800 		}
801 	}
802 
803 	///
804 	void putValue(bool b)
805 	{
806 		putKeyImpl();
807 		appender.put(b ? "true" : "false");
808 	}
809 
810 	///
811 	void putValue(scope const char[] value)
812 	{
813 		putKeyImpl();
814 		auto s = stringBegin();
815 		putStringPart(value);
816 		stringEnd(s);
817 	}
818 
819 	///
820 	void putValue(Timestamp t)
821 	{
822 		/*
823 		# offset datetime
824 		odt1 = 1979-05-27T07:32:00Z
825 		odt2 = 1979-05-27T00:32:00-07:00
826 		odt3 = 1979-05-27T00:32:00.999999-07:00
827 
828 		# local datetime
829 		ldt1 = 1979-05-27T07:32:00
830 		ldt2 = 1979-05-27T00:32:00.999999
831 
832 		# local date
833 		ld1 = 1979-05-27
834 
835 		# local time
836 		lt1 = 07:32:00
837 		lt2 = 00:32:00.999999
838 		*/
839 
840 		putKeyImpl();
841 		putTimestamp(*appender, t);
842 	}
843 
844 	///
845 	int serdeTarget() nothrow const @property
846 	{
847 		return -1;
848 	}
849 }
850 
851 ///
852 void serializeToml(Appender, V)(scope ref Appender appender, scope auto ref V value, TOMLBeautyConfig config = TOMLBeautyConfig.none)
853 	if (isOutputRange!(Appender, const(char)[]) && isOutputRange!(Appender, char))
854 {
855 	static assert(is(V == struct), "serializeToml only works on structs!");
856 
857 	import mir.ser : serializeValue;
858 	scope TOMLSerializer!Appender serializer;
859 	serializer.beautyConfig = config;
860 	serializer.appender = (() @trusted => &appender)();
861 	serializeValue(serializer, value);
862 }
863 
864 /++
865 JSON serialization function with pretty formatting.
866 +/
867 string serializeToml(V)(scope auto ref const V value, TOMLBeautyConfig config = TOMLBeautyConfig.none)
868 {
869 	import std.array: appender;
870 	import mir.functional: forward;
871 
872 	auto app = appender!(char[]);
873 	serializeToml(app, value, config);
874 	return (()@trusted => cast(string) app.data)();
875 }
876 
877 private static void putTimestamp(Appender)(ref Appender w, Timestamp t)
878 {
879 	if (t.offset)
880 	{
881 		assert(-24 * 60 <= t.offset && t.offset <= 24 * 60, "Offset absolute value should be less or equal to 24 * 60");
882 		assert(t.precision >= Timestamp.Precision.minute, "Offsets are not allowed on date values.");
883 		t.addMinutes(t.offset);
884 	}
885 
886 	if (t.precision < Timestamp.Precision.day)
887 		throw new IonException("Timestamps with only year or month precision are not supported in TOML serialization");
888 
889 	if (t.precision == Timestamp.Precision.minute)
890 	{
891 		t.second = 0;
892 		t.precision = Timestamp.Precision.second;
893 	}
894 
895 	if (!t.isOnlyTime)
896 	{
897 		printZeroPad(w, t.year, 4);
898 		w.put('-');
899 		printZeroPad(w, cast(uint)t.month, 2);
900 		w.put('-');
901 		printZeroPad(w, cast(uint)t.day, 2);
902 
903 		if (t.precision == Timestamp.Precision.day)
904 			return;
905 		w.put('T');
906 	}
907 
908 	printZeroPad(w, t.hour, 2);
909 	w.put(':');
910 	printZeroPad(w, t.minute, 2);
911 	w.put(':');
912 	printZeroPad(w, t.second, 2);
913 
914 	if (t.precision > Timestamp.Precision.second
915 		&& (t.fractionExponent < 0 && t.fractionCoefficient))
916 	{
917 		w.put('.');
918 		printZeroPad(w, t.fractionCoefficient, -int(t.fractionExponent));
919 	}
920 
921 	if (t.isLocalTime)
922 		return;
923 
924 	if (t.offset == 0)
925 	{
926 		w.put('Z');
927 		return;
928 	}
929 
930 	bool sign = t.offset < 0;
931 	uint absoluteOffset = !sign ? t.offset : -int(t.offset);
932 	uint offsetHour = absoluteOffset / 60u;
933 	uint offsetMinute = absoluteOffset % 60u;
934 
935 	w.put(sign ? '-' : '+');
936 	printZeroPad(w, offsetHour, 2);
937 	w.put(':');
938 	printZeroPad(w, offsetMinute, 2);
939 }