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 }