1 // Written in the D programming language. 2 3 /** 4 * 5 * Tom's Obvious, Minimal Language (v0.4.0). 6 * 7 * License: $(HTTP https://github.com/Kripth/toml/blob/master/LICENSE, MIT) 8 * Authors: Kripth 9 * References: $(LINK https://github.com/toml-lang/toml/blob/master/README.md) 10 * Source: $(HTTP https://github.com/Kripth/toml/blob/master/src/toml/toml.d, toml/_toml.d) 11 * 12 */ 13 module toml.toml; 14 15 import std.algorithm : canFind, min, stripRight; 16 import std.array : Appender; 17 import std.ascii : newline; 18 import std.conv : to; 19 import std.datetime : SysTime, DateTimeD = DateTime, Date, 20 TimeOfDayD = TimeOfDay; 21 import std.exception : enforce, assertThrown; 22 import std.math : isNaN, isFinite; 23 import std..string : join, strip, replace, indexOf; 24 import std.traits : isNumeric, isIntegral, isFloatingPoint, isArray, 25 isAssociativeArray, KeyType; 26 import std.typecons : Tuple; 27 import std.utf : encode, UseReplacementDchar; 28 29 import toml.datetime : DateTime, TimeOfDay; 30 31 /** 32 * Flags that control how a TOML document is parsed and encoded. 33 */ 34 enum TOMLOptions 35 { 36 37 none = 0x00, 38 unquotedStrings = 0x01, /// allow unquoted strings as values when parsing 39 40 } 41 42 /** 43 * TOML type enumeration. 44 */ 45 enum TOML_TYPE : byte 46 { 47 48 STRING, /// Indicates the type of a TOMLValue. 49 INTEGER, /// ditto 50 FLOAT, /// ditto 51 OFFSET_DATETIME, /// ditto 52 LOCAL_DATETIME, /// ditto 53 LOCAL_DATE, /// ditto 54 LOCAL_TIME, /// ditto 55 ARRAY, /// ditto 56 TABLE, /// ditto 57 TRUE, /// ditto 58 FALSE /// ditto 59 60 } 61 62 /** 63 * Main table of a TOML document. 64 * It works as a TOMLValue with the TOML_TYPE.TABLE type. 65 */ 66 struct TOMLDocument 67 { 68 69 public TOMLValue[string] table; 70 71 public this(TOMLValue[string] table) 72 { 73 this.table = table; 74 } 75 76 public this(TOMLValue value) 77 { 78 this(value.table); 79 } 80 81 public string toString() 82 { 83 Appender!string appender; 84 foreach (key, value; this.table) 85 { 86 appender.put(formatKey(key)); 87 appender.put(" = "); 88 value.append(appender); 89 appender.put(newline); 90 } 91 return appender.data; 92 } 93 94 alias table this; 95 96 } 97 98 /** 99 * Value of a TOML value. 100 */ 101 struct TOMLValue 102 { 103 104 private union Store 105 { 106 string str; 107 long integer; 108 double floating; 109 SysTime offsetDatetime; 110 DateTime localDatetime; 111 Date localDate; 112 TimeOfDay localTime; 113 TOMLValue[] array; 114 TOMLValue[string] table; 115 } 116 117 private Store store; 118 private TOML_TYPE _type; 119 120 public this(T)(T value) 121 { 122 static if (is(T == TOML_TYPE)) 123 { 124 this._type = value; 125 } 126 else 127 { 128 this.assign(value); 129 } 130 } 131 132 public inout pure nothrow @property @safe @nogc TOML_TYPE type() 133 { 134 return this._type; 135 } 136 137 /** 138 * Throws: TOMLException if type is not TOML_TYPE.STRING 139 */ 140 public inout @property @trusted string str() 141 { 142 enforce!TOMLException(this._type == TOML_TYPE.STRING, "TOMLValue is not a string"); 143 return this.store.str; 144 } 145 146 /** 147 * Throws: TOMLException if type is not TOML_TYPE.INTEGER 148 */ 149 public inout @property @trusted long integer() 150 { 151 enforce!TOMLException(this._type == TOML_TYPE.INTEGER, "TOMLValue is not an integer"); 152 return this.store.integer; 153 } 154 155 /** 156 * Throws: TOMLException if type is not TOML_TYPE.FLOAT 157 */ 158 public inout @property @trusted double floating() 159 { 160 enforce!TOMLException(this._type == TOML_TYPE.FLOAT, "TOMLValue is not a float"); 161 return this.store.floating; 162 } 163 164 /** 165 * Throws: TOMLException if type is not TOML_TYPE.OFFSET_DATETIME 166 */ 167 public @property ref SysTime offsetDatetime() return 168 { 169 enforce!TOMLException(this.type == TOML_TYPE.OFFSET_DATETIME, 170 "TOMLValue is not an offset datetime"); 171 return this.store.offsetDatetime; 172 } 173 174 /** 175 * Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATETIME 176 */ 177 public @property @trusted ref DateTime localDatetime() return 178 { 179 enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATETIME, 180 "TOMLValue is not a local datetime"); 181 return this.store.localDatetime; 182 } 183 184 /** 185 * Throws: TOMLException if type is not TOML_TYPE.LOCAL_DATE 186 */ 187 public @property @trusted ref Date localDate() return 188 { 189 enforce!TOMLException(this._type == TOML_TYPE.LOCAL_DATE, "TOMLValue is not a local date"); 190 return this.store.localDate; 191 } 192 193 /** 194 * Throws: TOMLException if type is not TOML_TYPE.LOCAL_TIME 195 */ 196 public @property @trusted ref TimeOfDay localTime() return 197 { 198 enforce!TOMLException(this._type == TOML_TYPE.LOCAL_TIME, "TOMLValue is not a local time"); 199 return this.store.localTime; 200 } 201 202 /** 203 * Throws: TOMLException if type is not TOML_TYPE.ARRAY 204 */ 205 public @property @trusted ref TOMLValue[] array() return 206 { 207 enforce!TOMLException(this._type == TOML_TYPE.ARRAY, "TOMLValue is not an array"); 208 return this.store.array; 209 } 210 211 /** 212 * Throws: TOMLException if type is not TOML_TYPE.TABLE 213 */ 214 public @property @trusted ref TOMLValue[string] table() return 215 { 216 enforce!TOMLException(this._type == TOML_TYPE.TABLE, "TOMLValue is not a table"); 217 return this.store.table; 218 } 219 220 public TOMLValue opIndex(size_t index) 221 { 222 return this.array[index]; 223 } 224 225 public TOMLValue* opBinaryRight(string op : "in")(string key) 226 { 227 return key in this.table; 228 } 229 230 public TOMLValue opIndex(string key) 231 { 232 return this.table[key]; 233 } 234 235 public int opApply(scope int delegate(string, ref TOMLValue) dg) 236 { 237 enforce!TOMLException(this._type == TOML_TYPE.TABLE, "TOMLValue is not a table"); 238 int result; 239 foreach (string key, ref value; this.store.table) 240 { 241 result = dg(key, value); 242 if (result) 243 break; 244 } 245 return result; 246 } 247 248 public void opAssign(T)(T value) 249 { 250 this.assign(value); 251 } 252 253 private void assign(T)(T value) 254 { 255 static if (is(T == TOMLValue)) 256 { 257 this.store = value.store; 258 this._type = value._type; 259 } 260 else static if (is(T : string)) 261 { 262 this.store.str = value; 263 this._type = TOML_TYPE.STRING; 264 } 265 else static if (isIntegral!T) 266 { 267 this.store.integer = value; 268 this._type = TOML_TYPE.INTEGER; 269 } 270 else static if (isFloatingPoint!T) 271 { 272 this.store.floating = value.to!double; 273 this._type = TOML_TYPE.FLOAT; 274 } 275 else static if (is(T == SysTime)) 276 { 277 this.store.offsetDatetime = value; 278 this._type = TOML_TYPE.OFFSET_DATETIME; 279 } 280 else static if (is(T == DateTime)) 281 { 282 this.store.localDatetime = value; 283 this._type = TOML_TYPE.LOCAL_DATETIME; 284 } 285 else static if (is(T == DateTimeD)) 286 { 287 this.store.localDatetime = DateTime(value.date, TimeOfDay(value.timeOfDay)); 288 this._type = TOML_TYPE.LOCAL_DATETIME; 289 } 290 else static if (is(T == Date)) 291 { 292 this.store.localDate = value; 293 this._type = TOML_TYPE.LOCAL_DATE; 294 } 295 else static if (is(T == TimeOfDay)) 296 { 297 this.store.localTime = value; 298 this._type = TOML_TYPE.LOCAL_TIME; 299 } 300 else static if (is(T == TimeOfDayD)) 301 { 302 this.store.localTime = TimeOfDay(value); 303 this._type = TOML_TYPE.LOCAL_TIME; 304 } 305 else static if (isArray!T) 306 { 307 static if (is(T == TOMLValue[])) 308 { 309 if (value.length) 310 { 311 // verify that every element has the same type 312 TOML_TYPE cmp = value[0].type; 313 foreach (element; value[1 .. $]) 314 { 315 enforce!TOMLException(element.type == cmp, 316 "Array's values must be of the same type"); 317 } 318 } 319 alias data = value; 320 } 321 else 322 { 323 TOMLValue[] data; 324 foreach (element; value) 325 { 326 data ~= TOMLValue(element); 327 } 328 } 329 this.store.array = data; 330 this._type = TOML_TYPE.ARRAY; 331 } 332 else static if (isAssociativeArray!T && is(KeyType!T : string)) 333 { 334 static if (is(T == TOMLValue[string])) 335 { 336 alias data = value; 337 } 338 else 339 { 340 TOMLValue[string] data; 341 foreach (key, v; value) 342 { 343 data[key] = v; 344 } 345 } 346 this.store.table = data; 347 this._type = TOML_TYPE.TABLE; 348 } 349 else static if (is(T == bool)) 350 { 351 _type = value ? TOML_TYPE.TRUE : TOML_TYPE.FALSE; 352 } 353 else 354 { 355 static assert(0); 356 } 357 } 358 359 public bool opEquals(T)(T value) 360 { 361 static if (is(T == TOMLValue)) 362 { 363 if (this._type != value._type) 364 return false; 365 final switch (this.type) with (TOML_TYPE) 366 { 367 case STRING: 368 return this.store.str == value.store.str; 369 case INTEGER: 370 return this.store.integer == value.store.integer; 371 case FLOAT: 372 return this.store.floating == value.store.floating; 373 case OFFSET_DATETIME: 374 return this.store.offsetDatetime == value.store.offsetDatetime; 375 case LOCAL_DATETIME: 376 return this.store.localDatetime == value.store.localDatetime; 377 case LOCAL_DATE: 378 return this.store.localDate == value.store.localDate; 379 case LOCAL_TIME: 380 return this.store.localTime == value.store.localTime; 381 case ARRAY: 382 return this.store.array == value.store.array; 383 //case TABLE: return this.store.table == value.store.table; // causes errors 384 case TABLE: 385 return this.opEquals(value.store.table); 386 case TRUE: 387 case FALSE: 388 return true; 389 } 390 } 391 else static if (is(T : string)) 392 { 393 return this._type == TOML_TYPE.STRING && this.store.str == value; 394 } 395 else static if (isNumeric!T) 396 { 397 if (this._type == TOML_TYPE.INTEGER) 398 return this.store.integer == value; 399 else if (this._type == TOML_TYPE.FLOAT) 400 return this.store.floating == value; 401 else 402 return false; 403 } 404 else static if (is(T == SysTime)) 405 { 406 return this._type == TOML_TYPE.OFFSET_DATETIME && this.store.offsetDatetime == value; 407 } 408 else static if (is(T == DateTime)) 409 { 410 return this._type == TOML_TYPE.LOCAL_DATETIME && this.store.localDatetime.dateTime == value.dateTime 411 && this.store.localDatetime.timeOfDay.fracSecs == value.timeOfDay.fracSecs; 412 } 413 else static if (is(T == DateTimeD)) 414 { 415 return this._type == TOML_TYPE.LOCAL_DATETIME 416 && this.store.localDatetime.dateTime == value; 417 } 418 else static if (is(T == Date)) 419 { 420 return this._type == TOML_TYPE.LOCAL_DATE && this.store.localDate == value; 421 } 422 else static if (is(T == TimeOfDay)) 423 { 424 return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime.timeOfDay == value.timeOfDay 425 && this.store.localTime.fracSecs == value.fracSecs; 426 } 427 else static if (is(T == TimeOfDayD)) 428 { 429 return this._type == TOML_TYPE.LOCAL_TIME && this.store.localTime == value; 430 } 431 else static if (isArray!T) 432 { 433 if (this._type != TOML_TYPE.ARRAY || this.store.array.length != value.length) 434 return false; 435 foreach (i, element; this.store.array) 436 { 437 if (element != value[i]) 438 return false; 439 } 440 return true; 441 } 442 else static if (isAssociativeArray!T && is(KeyType!T : string)) 443 { 444 if (this._type != TOML_TYPE.TABLE || this.store.table.length != value.length) 445 return false; 446 foreach (key, v; this.store.table) 447 { 448 auto cmp = key in value; 449 if (cmp is null || v != *cmp) 450 return false; 451 } 452 return true; 453 } 454 else static if (is(T == bool)) 455 { 456 return value ? _type == TOML_TYPE.TRUE : _type == TOML_TYPE.FALSE; 457 } 458 else 459 { 460 return false; 461 } 462 } 463 464 public void append(ref Appender!string appender) 465 { 466 final switch (this._type) with (TOML_TYPE) 467 { 468 case STRING: 469 appender.put(formatString(this.store.str)); 470 break; 471 case INTEGER: 472 appender.put(this.store.integer.to!string); 473 break; 474 case FLOAT: 475 immutable str = this.store.floating.to!string; 476 appender.put(str); 477 if (!str.canFind('.') && !str.canFind('e')) 478 appender.put(".0"); 479 break; 480 case OFFSET_DATETIME: 481 appender.put(this.store.offsetDatetime.toISOExtString()); 482 break; 483 case LOCAL_DATETIME: 484 appender.put(this.store.localDatetime.toISOExtString()); 485 break; 486 case LOCAL_DATE: 487 appender.put(this.store.localDate.toISOExtString()); 488 break; 489 case LOCAL_TIME: 490 appender.put(this.store.localTime.toISOExtString()); 491 break; 492 case ARRAY: 493 appender.put("["); 494 foreach (i, value; this.store.array) 495 { 496 value.append(appender); 497 if (i + 1 < this.store.array.length) 498 appender.put(", "); 499 } 500 appender.put("]"); 501 break; 502 case TABLE: 503 // display as an inline table 504 appender.put("{ "); 505 size_t i = 0; 506 foreach (key, value; this.store.table) 507 { 508 appender.put(formatKey(key)); 509 appender.put(" = "); 510 value.append(appender); 511 if (++i != this.store.table.length) 512 appender.put(", "); 513 } 514 appender.put(" }"); 515 break; 516 case TRUE: 517 appender.put("true"); 518 break; 519 case FALSE: 520 appender.put("false"); 521 break; 522 } 523 } 524 525 public string toString() 526 { 527 Appender!string appender; 528 this.append(appender); 529 return appender.data; 530 } 531 532 } 533 534 private string formatKey(string str) 535 { 536 foreach (c; str) 537 { 538 if ((c < '0' || c > '9') && (c < 'A' || c > 'Z') && (c < 'a' || c > 'z') 539 && c != '-' && c != '_') 540 return formatString(str); 541 } 542 return str; 543 } 544 545 private string formatString(string str) 546 { 547 Appender!string appender; 548 foreach (c; str) 549 { 550 switch (c) 551 { 552 case '"': 553 appender.put("\\\""); 554 break; 555 case '\\': 556 appender.put("\\\\"); 557 break; 558 case '\b': 559 appender.put("\\b"); 560 break; 561 case '\f': 562 appender.put("\\f"); 563 break; 564 case '\n': 565 appender.put("\\n"); 566 break; 567 case '\r': 568 appender.put("\\r"); 569 break; 570 case '\t': 571 appender.put("\\t"); 572 break; 573 default: 574 appender.put(c); 575 } 576 } 577 return "\"" ~ appender.data ~ "\""; 578 } 579 580 /** 581 * Parses a TOML document. 582 * Returns: a TOMLDocument with the parsed data 583 * Throws: 584 * TOMLParserException when the document's syntax is incorrect 585 */ 586 TOMLDocument parseTOML(string data, TOMLOptions options = TOMLOptions.none) 587 { 588 589 size_t index = 0; 590 591 /** 592 * Throws a TOMLParserException at the current line and column. 593 */ 594 void error(string message) 595 { 596 if (index >= data.length) 597 index = data.length; 598 size_t i, line, column; 599 while (i < index) 600 { 601 if (data[i++] == '\n') 602 { 603 line++; 604 column = 0; 605 } 606 else 607 { 608 column++; 609 } 610 } 611 throw new TOMLParserException(message, line + 1, column); 612 } 613 614 /** 615 * Throws a TOMLParserException throught the error function if 616 * cond is false. 617 */ 618 void enforceParser(bool cond, lazy string message) 619 { 620 if (!cond) 621 { 622 error(message); 623 } 624 } 625 626 TOMLValue[string] _ret; 627 auto current = &_ret; 628 629 string[][] tableNames; 630 631 void setImpl(TOMLValue[string]* table, string[] keys, string[] original, TOMLValue value) 632 { 633 auto ptr = keys[0] in *table; 634 if (keys.length == 1) 635 { 636 // should not be there 637 enforceParser(ptr is null, "Key is already defined"); 638 (*table)[keys[0]] = value; 639 } 640 else 641 { 642 // must be a table 643 if (ptr !is null) 644 enforceParser((*ptr).type == TOML_TYPE.TABLE, join(original[0 .. $ - keys.length], 645 ".") ~ " is already defined and is not a table"); 646 else 647 (*table)[keys[0]] = (TOMLValue[string]).init; 648 setImpl(&((*table)[keys[0]].table()), keys[1 .. $], original, value); 649 } 650 } 651 652 void set(string[] keys, TOMLValue value) 653 { 654 setImpl(current, keys, keys, value); 655 } 656 657 /** 658 * Removes whitespace characters and comments. 659 * Return: whether there's still data to read 660 */ 661 bool clear(bool clear_newline = true)() 662 { 663 static if (clear_newline) 664 { 665 enum chars = " \t\r\n"; 666 } 667 else 668 { 669 enum chars = " \t\r"; 670 } 671 if (index < data.length) 672 { 673 if (chars.canFind(data[index])) 674 { 675 index++; 676 return clear!clear_newline(); 677 } 678 else if (data[index] == '#') 679 { 680 // skip until end of line 681 while (++index < data.length && data[index] != '\n') 682 { 683 } 684 static if (clear_newline) 685 { 686 index++; // point at the next character 687 return clear(); 688 } 689 else 690 { 691 return true; 692 } 693 } 694 else 695 { 696 return true; 697 } 698 } 699 else 700 { 701 return false; 702 } 703 } 704 705 /** 706 * Indicates whether the given character is valid in an unquoted key. 707 */ 708 bool isValidKeyChar(immutable char c) 709 { 710 return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' 711 && c <= '9' || c == '-' || c == '_'; 712 } 713 714 string readQuotedString(bool multiline)() 715 { 716 Appender!string ret; 717 bool backslash = false; 718 while (index < data.length) 719 { 720 static if (!multiline) 721 { 722 enforceParser(data[index] != '\n', "Unterminated quoted string"); 723 } 724 if (backslash) 725 { 726 void readUnicode(size_t size)() 727 { 728 enforceParser(index + size < data.length, "Invalid UTF-8 sequence"); 729 char[4] buffer; 730 immutable len = encode!(UseReplacementDchar.yes)(buffer, 731 cast(dchar) to!ulong(data[index + 1 .. index + 1 + size], 16)); 732 ret.put(buffer[0 .. len].idup); 733 index += size; 734 } 735 736 switch (data[index]) 737 { 738 case '"': 739 ret.put('"'); 740 break; 741 case '\\': 742 ret.put('\\'); 743 break; 744 case 'b': 745 ret.put('\b'); 746 break; 747 case 't': 748 ret.put('\t'); 749 break; 750 case 'n': 751 ret.put('\n'); 752 break; 753 case 'f': 754 ret.put('\f'); 755 break; 756 case 'r': 757 ret.put('\r'); 758 break; 759 case 'u': 760 readUnicode!4(); 761 break; 762 case 'U': 763 readUnicode!8(); 764 break; 765 default: 766 static if (multiline) 767 { 768 index++; 769 if (clear()) 770 { 771 // remove whitespace characters until next valid character 772 index--; 773 break; 774 } 775 } 776 enforceParser(false, "Invalid escape sequence: '\\" ~ (index < data.length 777 ? [data[index]] : "EOF") ~ "'"); 778 } 779 backslash = false; 780 } 781 else 782 { 783 if (data[index] == '\\') 784 { 785 backslash = true; 786 } 787 else if (data[index] == '"') 788 { 789 // string closed 790 index++; 791 static if (multiline) 792 { 793 // control that the string is really closed 794 if (index + 2 <= data.length && data[index .. index + 2] == "\"\"") 795 { 796 index += 2; 797 return ret.data.stripFirstLine; 798 } 799 else 800 { 801 ret.put("\""); 802 continue; 803 } 804 } 805 else 806 { 807 return ret.data; 808 } 809 } 810 else 811 { 812 static if (multiline) 813 { 814 mixin(doLineConversion); 815 } 816 ret.put(data[index]); 817 } 818 } 819 index++; 820 } 821 error("Expecting \" (double quote) but found EOF"); 822 assert(0); 823 } 824 825 string readSimpleQuotedString(bool multiline)() 826 { 827 Appender!string ret; 828 while (index < data.length) 829 { 830 static if (!multiline) 831 { 832 enforceParser(data[index] != '\n', "Unterminated quoted string"); 833 } 834 if (data[index] == '\'') 835 { 836 // closed 837 index++; 838 static if (multiline) 839 { 840 // there must be 3 of them 841 if (index + 2 <= data.length && data[index .. index + 2] == "''") 842 { 843 index += 2; 844 return ret.data.stripFirstLine; 845 } 846 else 847 { 848 ret.put("'"); 849 } 850 } 851 else 852 { 853 return ret.data; 854 } 855 } 856 else 857 { 858 static if (multiline) 859 { 860 mixin(doLineConversion); 861 } 862 ret.put(data[index++]); 863 } 864 } 865 error("Expecting ' (single quote) but found EOF"); 866 assert(0); 867 } 868 869 string removeUnderscores(string str, string[] ranges...) 870 { 871 bool checkRange(char c) 872 { 873 foreach (range; ranges) 874 { 875 if (c >= range[0] && c <= range[1]) 876 return true; 877 } 878 return false; 879 } 880 881 bool underscore = false; 882 for (size_t i = 0; i < str.length; i++) 883 { 884 if (str[i] == '_') 885 { 886 if (underscore || i == 0 || i == str.length - 1 887 || !checkRange(str[i - 1]) || !checkRange(str[i + 1])) 888 throw new Exception(""); 889 str = str[0 .. i] ~ str[i + 1 .. $]; 890 i--; 891 underscore = true; 892 } 893 else 894 { 895 underscore = false; 896 } 897 } 898 return str; 899 } 900 901 TOMLValue readSpecial() 902 { 903 immutable start = index; 904 while (index < data.length && !"\t\r\n,]}#".canFind(data[index])) 905 index++; 906 string ret = data[start .. index].stripRight(' '); 907 enforceParser(ret.length > 0, "Invalid empty value"); 908 switch (ret) 909 { 910 case "true": 911 return TOMLValue(true); 912 case "false": 913 return TOMLValue(false); 914 case "inf": 915 case "+inf": 916 return TOMLValue(double.infinity); 917 case "-inf": 918 return TOMLValue(-double.infinity); 919 case "nan": 920 case "+nan": 921 return TOMLValue(double.nan); 922 case "-nan": 923 return TOMLValue(-double.nan); 924 default: 925 immutable original = ret; 926 try 927 { 928 if (ret.length >= 10 && ret[4] == '-' && ret[7] == '-') 929 { 930 // date or datetime 931 if (ret.length >= 19 && (ret[10] == 'T' || ret[10] == ' ') 932 && ret[13] == ':' && ret[16] == ':') 933 { 934 // datetime 935 if (ret[10] == ' ') 936 ret = ret[0 .. 10] ~ 'T' ~ ret[11 .. $]; 937 if (ret[19 .. $].canFind("-") || ret[$ - 1] == 'Z') 938 { 939 // has timezone 940 return TOMLValue(SysTime.fromISOExtString(ret)); 941 } 942 else 943 { 944 // is space allowed instead of T? 945 return TOMLValue(DateTime.fromISOExtString(ret)); 946 } 947 } 948 else 949 { 950 return TOMLValue(Date.fromISOExtString(ret)); 951 } 952 } 953 else if (ret.length >= 8 && ret[2] == ':' && ret[5] == ':') 954 { 955 return TOMLValue(TimeOfDay.fromISOExtString(ret)); 956 } 957 if (ret.length > 2 && ret[0] == '0') 958 { 959 switch (ret[1]) 960 { 961 case 'x': 962 return TOMLValue(to!long(removeUnderscores(ret[2 .. $], 963 "09", "AZ", "az"), 16)); 964 case 'o': 965 return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "08"), 8)); 966 case 'b': 967 return TOMLValue(to!long(removeUnderscores(ret[2 .. $], "01"), 2)); 968 default: 969 break; 970 } 971 } 972 if (ret.canFind('.') || ret.canFind('e') || ret.canFind('E')) 973 { 974 return TOMLValue(to!double(removeUnderscores(ret, "09"))); 975 } 976 else 977 { 978 if (ret[0] != '0' || ret.length == 1) 979 return TOMLValue(to!long(removeUnderscores(ret, "09"))); 980 } 981 } 982 catch (Exception) 983 { 984 } 985 // not a valid value at this point 986 if (options & TOMLOptions.unquotedStrings) 987 return TOMLValue(original); 988 else 989 error("Invalid type: '" ~ original ~ "'"); 990 assert(0); 991 } 992 } 993 994 string readKey() 995 { 996 enforceParser(index < data.length, "Key declaration expected but found EOF"); 997 string ret; 998 if (data[index] == '"') 999 { 1000 index++; 1001 ret = readQuotedString!false(); 1002 } 1003 else if (data[index] == '\'') 1004 { 1005 index++; 1006 ret = readSimpleQuotedString!false(); 1007 } 1008 else 1009 { 1010 Appender!string appender; 1011 while (index < data.length && isValidKeyChar(data[index])) 1012 { 1013 appender.put(data[index++]); 1014 } 1015 ret = appender.data; 1016 enforceParser(ret.length != 0, "Key is empty or contains invalid characters"); 1017 } 1018 return ret; 1019 } 1020 1021 string[] readKeys() 1022 { 1023 string[] keys; 1024 index--; 1025 do 1026 { 1027 index++; 1028 clear!false(); 1029 keys ~= readKey(); 1030 clear!false(); 1031 } 1032 while (index < data.length && data[index] == '.'); 1033 enforceParser(keys.length != 0, "Key cannot be empty"); 1034 return keys; 1035 } 1036 1037 TOMLValue readValue() 1038 { 1039 if (index < data.length) 1040 { 1041 switch (data[index++]) 1042 { 1043 case '"': 1044 if (index + 2 <= data.length && data[index .. index + 2] == "\"\"") 1045 { 1046 index += 2; 1047 return TOMLValue(readQuotedString!true()); 1048 } 1049 else 1050 { 1051 return TOMLValue(readQuotedString!false()); 1052 } 1053 case '\'': 1054 if (index + 2 <= data.length && data[index .. index + 2] == "''") 1055 { 1056 index += 2; 1057 return TOMLValue(readSimpleQuotedString!true()); 1058 } 1059 else 1060 { 1061 return TOMLValue(readSimpleQuotedString!false()); 1062 } 1063 case '[': 1064 clear(); 1065 TOMLValue[] array; 1066 bool comma = true; 1067 while (data[index] != ']') 1068 { //TODO check range error 1069 enforceParser(comma, "Elements of the array must be separated with a comma"); 1070 array ~= readValue(); 1071 clear!false(); // spaces allowed between elements and commas 1072 if (data[index] == ',') 1073 { //TODO check range error 1074 index++; 1075 comma = true; 1076 } 1077 else 1078 { 1079 comma = false; 1080 } 1081 clear(); // spaces and newlines allowed between elements 1082 } 1083 index++; 1084 return TOMLValue(array); 1085 case '{': 1086 clear!false(); 1087 TOMLValue[string] table; 1088 bool comma = true; 1089 while (data[index] != '}') 1090 { //TODO check range error 1091 enforceParser(comma, "Elements of the table must be separated with a comma"); 1092 auto keys = readKeys(); 1093 enforceParser(clear!false() && data[index++] == '=' 1094 && clear!false(), "Expected value after key declaration"); 1095 setImpl(&table, keys, keys, readValue()); 1096 enforceParser(clear!false(), 1097 "Expected ',' or '}' but found " ~ (index < data.length ? "EOL" : "EOF")); 1098 if (data[index] == ',') 1099 { 1100 index++; 1101 comma = true; 1102 } 1103 else 1104 { 1105 comma = false; 1106 } 1107 clear!false(); 1108 } 1109 index++; 1110 return TOMLValue(table); 1111 default: 1112 index--; 1113 break; 1114 } 1115 } 1116 return readSpecial(); 1117 } 1118 1119 void readKeyValue(string[] keys) 1120 { 1121 if (clear()) 1122 { 1123 enforceParser(data[index++] == '=', "Expected '=' after key declaration"); 1124 if (clear!false()) 1125 { 1126 set(keys, readValue()); 1127 // there must be nothing after the key/value declaration except comments and whitespaces 1128 if (clear!false()) 1129 enforceParser(data[index] == '\n', 1130 "Invalid characters after value declaration: " ~ data[index]); 1131 } 1132 else 1133 { 1134 //TODO throw exception (missing value) 1135 } 1136 } 1137 else 1138 { 1139 //TODO throw exception (missing value) 1140 } 1141 } 1142 1143 void next() 1144 { 1145 1146 if (data[index] == '[') 1147 { 1148 current = &_ret; // reset base 1149 index++; 1150 bool array = false; 1151 if (index < data.length && data[index] == '[') 1152 { 1153 index++; 1154 array = true; 1155 } 1156 string[] keys = readKeys(); 1157 enforceParser(index < data.length && data[index++] == ']', 1158 "Invalid " ~ (array ? "array" : "table") ~ " key declaration"); 1159 if (array) 1160 enforceParser(index < data.length && data[index++] == ']', 1161 "Invalid array key declaration"); 1162 if (!array) 1163 { 1164 //TODO only enforce if every key is a table 1165 enforceParser(!tableNames.canFind(keys), 1166 "Table name has already been directly defined"); 1167 tableNames ~= keys; 1168 } 1169 void update(string key, bool allowArray = true) 1170 { 1171 if (key !in *current) 1172 set([key], TOMLValue(TOML_TYPE.TABLE)); 1173 auto ret = (*current)[key]; 1174 if (ret.type == TOML_TYPE.TABLE) 1175 current = &((*current)[key].table()); 1176 else if (allowArray && ret.type == TOML_TYPE.ARRAY) 1177 current = &((*current)[key].array[$ - 1].table()); 1178 else 1179 error("Invalid type"); 1180 } 1181 1182 foreach (immutable key; keys[0 .. $ - 1]) 1183 { 1184 update(key); 1185 } 1186 if (array) 1187 { 1188 auto exist = keys[$ - 1] in *current; 1189 if (exist) 1190 { 1191 //TODO must be an array 1192 (*exist).array ~= TOMLValue(TOML_TYPE.TABLE); 1193 } 1194 else 1195 { 1196 set([keys[$ - 1]], TOMLValue([TOMLValue(TOML_TYPE.TABLE)])); 1197 } 1198 current = &((*current)[keys[$ - 1]].array[$ - 1].table()); 1199 } 1200 else 1201 { 1202 update(keys[$ - 1], false); 1203 } 1204 } 1205 else 1206 { 1207 readKeyValue(readKeys()); 1208 } 1209 1210 } 1211 1212 while (clear()) 1213 { 1214 next(); 1215 } 1216 1217 return TOMLDocument(_ret); 1218 1219 } 1220 1221 private @property string stripFirstLine(string data) 1222 { 1223 size_t i = 0; 1224 while (i < data.length && data[i] != '\n') 1225 i++; 1226 if (data[0 .. i].strip.length == 0) 1227 return data[i + 1 .. $]; 1228 else 1229 return data; 1230 } 1231 1232 version (Windows) 1233 { 1234 // convert posix's line ending to windows' 1235 private enum doLineConversion = q{ 1236 if(data[index] == '\n' && index != 0 && data[index-1] != '\r') { 1237 index++; 1238 ret.put("\r\n"); 1239 continue; 1240 } 1241 }; 1242 } 1243 else 1244 { 1245 // convert windows' line ending to posix's 1246 private enum doLineConversion = q{ 1247 if(data[index] == '\r' && index + 1 < data.length && data[index+1] == '\n') { 1248 index += 2; 1249 ret.put("\n"); 1250 continue; 1251 } 1252 }; 1253 } 1254 1255 unittest 1256 { 1257 1258 TOMLDocument doc; 1259 1260 // tests from the official documentation 1261 // https://github.com/toml-lang/toml/blob/master/README.md 1262 1263 doc = parseTOML(` 1264 # This is a TOML document. 1265 1266 title = "TOML Example" 1267 1268 [owner] 1269 name = "Tom Preston-Werner" 1270 dob = 1979-05-27T07:32:00-08:00 # First class dates 1271 1272 [database] 1273 server = "192.168.1.1" 1274 ports = [ 8001, 8001, 8002 ] 1275 connection_max = 5000 1276 enabled = true 1277 1278 [servers] 1279 1280 # Indentation (tabs and/or spaces) is allowed but not required 1281 [servers.alpha] 1282 ip = "10.0.0.1" 1283 dc = "eqdc10" 1284 1285 [servers.beta] 1286 ip = "10.0.0.2" 1287 dc = "eqdc10" 1288 1289 [clients] 1290 data = [ ["gamma", "delta"], [1, 2] ] 1291 1292 # Line breaks are OK when inside arrays 1293 hosts = [ 1294 "alpha", 1295 "omega" 1296 ] 1297 `); 1298 assert(doc["title"] == "TOML Example"); 1299 assert(doc["owner"]["name"] == "Tom Preston-Werner"); 1300 assert(doc["owner"]["dob"] == SysTime.fromISOExtString("1979-05-27T07:32:00-08:00")); 1301 assert(doc["database"]["server"] == "192.168.1.1"); 1302 assert(doc["database"]["ports"] == [8001, 8001, 8002]); 1303 assert(doc["database"]["connection_max"] == 5000); 1304 assert(doc["database"]["enabled"] == true); 1305 //TODO 1306 assert(doc["clients"]["data"][0] == ["gamma", "delta"]); 1307 assert(doc["clients"]["data"][1] == [1, 2]); 1308 assert(doc["clients"]["hosts"] == ["alpha", "omega"]); 1309 1310 doc = parseTOML(` 1311 # This is a full-line comment 1312 key = "value" 1313 `); 1314 assert("key" in doc); 1315 assert(doc["key"].type == TOML_TYPE.STRING); 1316 assert(doc["key"].str == "value"); 1317 1318 foreach (k, v; doc) 1319 { 1320 assert(k == "key"); 1321 assert(v.type == TOML_TYPE.STRING); 1322 assert(v.str == "value"); 1323 } 1324 1325 assertThrown!TOMLException({ parseTOML(`key = # INVALID`); }()); 1326 assertThrown!TOMLException({ parseTOML("key =\nkey2 = 'test'"); }()); 1327 1328 // ---- 1329 // Keys 1330 // ---- 1331 1332 // bare keys 1333 doc = parseTOML(` 1334 key = "value" 1335 bare_key = "value" 1336 bare-key = "value" 1337 1234 = "value" 1338 `); 1339 assert(doc["key"] == "value"); 1340 assert(doc["bare_key"] == "value"); 1341 assert(doc["bare-key"] == "value"); 1342 assert(doc["1234"] == "value"); 1343 1344 // quoted keys 1345 doc = parseTOML(` 1346 "127.0.0.1" = "value" 1347 "character encoding" = "value" 1348 "ʎǝʞ" = "value" 1349 'key2' = "value" 1350 'quoted "value"' = "value" 1351 `); 1352 assert(doc["127.0.0.1"] == "value"); 1353 assert(doc["character encoding"] == "value"); 1354 assert(doc["ʎǝʞ"] == "value"); 1355 assert(doc["key2"] == "value"); 1356 assert(doc["quoted \"value\""] == "value"); 1357 1358 // no key name 1359 assertThrown!TOMLException({ parseTOML(`= "no key name" # INVALID`); }()); 1360 1361 // empty key 1362 assert(parseTOML(`"" = "blank"`)[""] == "blank"); 1363 assert(parseTOML(`'' = 'blank'`)[""] == "blank"); 1364 1365 // dotted keys 1366 doc = parseTOML(` 1367 name = "Orange" 1368 physical.color = "orange" 1369 physical.shape = "round" 1370 site."google.com" = true 1371 `); 1372 assert(doc["name"] == "Orange"); 1373 assert(doc["physical"] == ["color" : "orange", "shape" : "round"]); 1374 assert(doc["site"]["google.com"] == true); 1375 1376 // ------ 1377 // String 1378 // ------ 1379 1380 // basic strings 1381 doc = parseTOML(`str = "I'm a string. \"You can quote me\". Name\tJos\u00E9\nLocation\tSF."`); 1382 assert(doc["str"] == "I'm a string. \"You can quote me\". Name\tJosé\nLocation\tSF."); 1383 1384 // multi-line basic strings 1385 doc = parseTOML(`str1 = """ 1386 Roses are red 1387 Violets are blue"""`); 1388 version (Posix) 1389 assert(doc["str1"] == "Roses are red\nViolets are blue"); 1390 else 1391 assert(doc["str1"] == "Roses are red\r\nViolets are blue"); 1392 1393 doc = parseTOML(` 1394 # The following strings are byte-for-byte equivalent: 1395 str1 = "The quick brown fox jumps over the lazy dog." 1396 1397 str2 = """ 1398 The quick brown \ 1399 1400 1401 fox jumps over \ 1402 the lazy dog.""" 1403 1404 str3 = """\ 1405 The quick brown \ 1406 fox jumps over \ 1407 the lazy dog.\ 1408 """`); 1409 assert(doc["str1"] == "The quick brown fox jumps over the lazy dog."); 1410 assert(doc["str1"] == doc["str2"]); 1411 assert(doc["str1"] == doc["str3"]); 1412 1413 // literal strings 1414 doc = parseTOML(` 1415 # What you see is what you get. 1416 winpath = 'C:\Users\nodejs\templates' 1417 winpath2 = '\\ServerX\admin$\system32\' 1418 quoted = 'Tom "Dubs" Preston-Werner' 1419 regex = '<\i\c*\s*>' 1420 `); 1421 assert(doc["winpath"] == `C:\Users\nodejs\templates`); 1422 assert(doc["winpath2"] == `\\ServerX\admin$\system32\`); 1423 assert(doc["quoted"] == `Tom "Dubs" Preston-Werner`); 1424 assert(doc["regex"] == `<\i\c*\s*>`); 1425 1426 // multi-line literal strings 1427 doc = parseTOML(` 1428 regex2 = '''I [dw]on't need \d{2} apples''' 1429 lines = ''' 1430 The first newline is 1431 trimmed in raw strings. 1432 All other whitespace 1433 is preserved. 1434 '''`); 1435 assert(doc["regex2"] == `I [dw]on't need \d{2} apples`); 1436 assert(doc["lines"] == "The first newline is" ~ newline ~ "trimmed in raw strings." 1437 ~ newline ~ " All other whitespace" ~ newline ~ " is preserved." ~ newline); 1438 1439 // ------- 1440 // Integer 1441 // ------- 1442 1443 doc = parseTOML(` 1444 int1 = +99 1445 int2 = 42 1446 int3 = 0 1447 int4 = -17 1448 `); 1449 assert(doc["int1"].type == TOML_TYPE.INTEGER); 1450 assert(doc["int1"].integer == 99); 1451 assert(doc["int2"] == 42); 1452 assert(doc["int3"] == 0); 1453 assert(doc["int4"] == -17); 1454 1455 doc = parseTOML(` 1456 int5 = 1_000 1457 int6 = 5_349_221 1458 int7 = 1_2_3_4_5 # VALID but discouraged 1459 `); 1460 assert(doc["int5"] == 1_000); 1461 assert(doc["int6"] == 5_349_221); 1462 assert(doc["int7"] == 1_2_3_4_5); 1463 1464 // leading 0s not allowed 1465 assertThrown!TOMLException({ parseTOML(`invalid = 01`); }()); 1466 1467 // underscores must be enclosed in numbers 1468 assertThrown!TOMLException({ parseTOML(`invalid = _123`); }()); 1469 assertThrown!TOMLException({ parseTOML(`invalid = 123_`); }()); 1470 assertThrown!TOMLException({ parseTOML(`invalid = 123__123`); }()); 1471 assertThrown!TOMLException({ parseTOML(`invalid = 0b01_21`); }()); 1472 assertThrown!TOMLException({ parseTOML(`invalid = 0x_deadbeef`); }()); 1473 assertThrown!TOMLException({ parseTOML(`invalid = 0b0101__00`); }()); 1474 1475 doc = parseTOML(` 1476 # hexadecimal with prefix 0x 1477 hex1 = 0xDEADBEEF 1478 hex2 = 0xdeadbeef 1479 hex3 = 0xdead_beef 1480 1481 # octal with prefix 0o 1482 oct1 = 0o01234567 1483 oct2 = 0o755 # useful for Unix file permissions 1484 1485 # binary with prefix 0b 1486 bin1 = 0b11010110 1487 `); 1488 assert(doc["hex1"] == 0xDEADBEEF); 1489 assert(doc["hex2"] == 0xdeadbeef); 1490 assert(doc["hex3"] == 0xdead_beef); 1491 assert(doc["oct1"] == 342391); 1492 assert(doc["oct2"] == 493); 1493 assert(doc["bin1"] == 0b11010110); 1494 1495 assertThrown!TOMLException({ parseTOML(`invalid = 0h111`); }()); 1496 1497 // ----- 1498 // Float 1499 // ----- 1500 1501 doc = parseTOML(` 1502 # fractional 1503 flt1 = +1.0 1504 flt2 = 3.1415 1505 flt3 = -0.01 1506 1507 # exponent 1508 flt4 = 5e+22 1509 flt5 = 1e6 1510 flt6 = -2E-2 1511 1512 # both 1513 flt7 = 6.626e-34 1514 `); 1515 assert(doc["flt1"].type == TOML_TYPE.FLOAT); 1516 assert(doc["flt1"].floating == 1); 1517 assert(doc["flt2"] == 3.1415); 1518 assert(doc["flt3"] == -.01); 1519 assert(doc["flt4"] == 5e+22); 1520 assert(doc["flt5"] == 1e6); 1521 assert(doc["flt6"] == -2E-2); 1522 assert(doc["flt7"] == 6.626e-34); 1523 1524 doc = parseTOML(`flt8 = 9_224_617.445_991_228_313`); 1525 assert(doc["flt8"] == 9_224_617.445_991_228_313); 1526 1527 doc = parseTOML(` 1528 # infinity 1529 sf1 = inf # positive infinity 1530 sf2 = +inf # positive infinity 1531 sf3 = -inf # negative infinity 1532 1533 # not a number 1534 sf4 = nan # actual sNaN/qNaN encoding is implementation specific 1535 sf5 = +nan # same as nan 1536 sf6 = -nan # valid, actual encoding is implementation specific 1537 `); 1538 assert(doc["sf1"] == double.infinity); 1539 assert(doc["sf2"] == double.infinity); 1540 assert(doc["sf3"] == -double.infinity); 1541 assert(doc["sf4"].floating.isNaN()); 1542 assert(doc["sf5"].floating.isNaN()); 1543 assert(doc["sf6"].floating.isNaN()); 1544 1545 // ------- 1546 // Boolean 1547 // ------- 1548 1549 doc = parseTOML(` 1550 bool1 = true 1551 bool2 = false 1552 `); 1553 assert(doc["bool1"].type == TOML_TYPE.TRUE); 1554 assert(doc["bool2"].type == TOML_TYPE.FALSE); 1555 assert(doc["bool1"] == true); 1556 assert(doc["bool2"] == false); 1557 1558 // ---------------- 1559 // Offset Date-Time 1560 // ---------------- 1561 1562 doc = parseTOML(` 1563 odt1 = 1979-05-27T07:32:00Z 1564 odt2 = 1979-05-27T00:32:00-07:00 1565 odt3 = 1979-05-27T00:32:00.999999-07:00 1566 `); 1567 assert(doc["odt1"].type == TOML_TYPE.OFFSET_DATETIME); 1568 assert(doc["odt1"].offsetDatetime == SysTime.fromISOExtString("1979-05-27T07:32:00Z")); 1569 assert(doc["odt2"] == SysTime.fromISOExtString("1979-05-27T00:32:00-07:00")); 1570 assert(doc["odt3"] == SysTime.fromISOExtString("1979-05-27T00:32:00.999999-07:00")); 1571 1572 doc = parseTOML(`odt4 = 1979-05-27 07:32:00Z`); 1573 assert(doc["odt4"] == SysTime.fromISOExtString("1979-05-27T07:32:00Z")); 1574 1575 // --------------- 1576 // Local Date-Time 1577 // --------------- 1578 1579 doc = parseTOML(` 1580 ldt1 = 1979-05-27T07:32:00 1581 ldt2 = 1979-05-27T00:32:00.999999 1582 `); 1583 assert(doc["ldt1"].type == TOML_TYPE.LOCAL_DATETIME); 1584 assert(doc["ldt1"].localDatetime == DateTime.fromISOExtString("1979-05-27T07:32:00")); 1585 assert(doc["ldt2"] == DateTime.fromISOExtString("1979-05-27T00:32:00.999999")); 1586 1587 // ---------- 1588 // Local Date 1589 // ---------- 1590 1591 doc = parseTOML(` 1592 ld1 = 1979-05-27 1593 `); 1594 assert(doc["ld1"].type == TOML_TYPE.LOCAL_DATE); 1595 assert(doc["ld1"].localDate == Date.fromISOExtString("1979-05-27")); 1596 1597 // ---------- 1598 // Local Time 1599 // ---------- 1600 1601 doc = parseTOML(` 1602 lt1 = 07:32:00 1603 lt2 = 00:32:00.999999 1604 `); 1605 assert(doc["lt1"].type == TOML_TYPE.LOCAL_TIME); 1606 assert(doc["lt1"].localTime == TimeOfDay.fromISOExtString("07:32:00")); 1607 assert(doc["lt2"] == TimeOfDay.fromISOExtString("00:32:00.999999")); 1608 assert(doc["lt2"].localTime.fracSecs.total!"msecs" == 999999); 1609 1610 // ----- 1611 // Array 1612 // ----- 1613 1614 doc = parseTOML(` 1615 arr1 = [ 1, 2, 3 ] 1616 arr2 = [ "red", "yellow", "green" ] 1617 arr3 = [ [ 1, 2 ], [3, 4, 5] ] 1618 arr4 = [ "all", 'strings', """are the same""", '''type'''] 1619 arr5 = [ [ 1, 2 ], ["a", "b", "c"] ] 1620 `); 1621 assert(doc["arr1"].type == TOML_TYPE.ARRAY); 1622 assert(doc["arr1"].array == [TOMLValue(1), TOMLValue(2), TOMLValue(3)]); 1623 assert(doc["arr2"] == ["red", "yellow", "green"]); 1624 assert(doc["arr3"] == [[1, 2], [3, 4, 5]]); 1625 assert(doc["arr4"] == ["all", "strings", "are the same", "type"]); 1626 assert(doc["arr5"] == [TOMLValue([1, 2]), TOMLValue(["a", "b", "c"])]); 1627 1628 assertThrown!TOMLException({ parseTOML(`arr6 = [ 1, 2.0 ]`); }()); 1629 1630 doc = parseTOML(` 1631 arr7 = [ 1632 1, 2, 3 1633 ] 1634 1635 arr8 = [ 1636 1, 1637 2, # this is ok 1638 ] 1639 `); 1640 assert(doc["arr7"] == [1, 2, 3]); 1641 assert(doc["arr8"] == [1, 2]); 1642 1643 // ----- 1644 // Table 1645 // ----- 1646 1647 doc = parseTOML(` 1648 [table-1] 1649 key1 = "some string" 1650 key2 = 123 1651 1652 [table-2] 1653 key1 = "another string" 1654 key2 = 456 1655 `); 1656 assert(doc["table-1"].type == TOML_TYPE.TABLE); 1657 assert(doc["table-1"] == ["key1" : TOMLValue("some string"), "key2" : TOMLValue(123)]); 1658 assert(doc["table-2"] == ["key1" : TOMLValue("another string"), "key2" : TOMLValue(456)]); 1659 1660 doc = parseTOML(` 1661 [dog."tater.man"] 1662 type.name = "pug" 1663 `); 1664 assert(doc["dog"]["tater.man"]["type"]["name"] == "pug"); 1665 1666 doc = parseTOML(` 1667 [a.b.c] # this is best practice 1668 [ d.e.f ] # same as [d.e.f] 1669 [ g . h . i ] # same as [g.h.i] 1670 [ j . "ʞ" . 'l' ] # same as [j."ʞ".'l'] 1671 `); 1672 assert(doc["a"]["b"]["c"].type == TOML_TYPE.TABLE); 1673 assert(doc["d"]["e"]["f"].type == TOML_TYPE.TABLE); 1674 assert(doc["g"]["h"]["i"].type == TOML_TYPE.TABLE); 1675 assert(doc["j"]["ʞ"]["l"].type == TOML_TYPE.TABLE); 1676 1677 doc = parseTOML(` 1678 # [x] you 1679 # [x.y] don't 1680 # [x.y.z] need these 1681 [x.y.z.w] # for this to work 1682 `); 1683 assert(doc["x"]["y"]["z"]["w"].type == TOML_TYPE.TABLE); 1684 1685 doc = parseTOML(` 1686 [a.b] 1687 c = 1 1688 1689 [a] 1690 d = 2 1691 `); 1692 assert(doc["a"]["b"]["c"] == 1); 1693 assert(doc["a"]["d"] == 2); 1694 1695 assertThrown!TOMLException({ parseTOML(` 1696 # DO NOT DO THIS 1697 1698 [a] 1699 b = 1 1700 1701 [a] 1702 c = 2 1703 `); }()); 1704 1705 assertThrown!TOMLException({ parseTOML(` 1706 # DO NOT DO THIS EITHER 1707 1708 [a] 1709 b = 1 1710 1711 [a.b] 1712 c = 2 1713 `); }()); 1714 1715 assertThrown!TOMLException({ parseTOML(`[]`); }()); 1716 assertThrown!TOMLException({ parseTOML(`[a.]`); }()); 1717 assertThrown!TOMLException({ parseTOML(`[a..b]`); }()); 1718 assertThrown!TOMLException({ parseTOML(`[.b]`); }()); 1719 assertThrown!TOMLException({ parseTOML(`[.]`); }()); 1720 1721 // ------------ 1722 // Inline Table 1723 // ------------ 1724 1725 doc = parseTOML(` 1726 name = { first = "Tom", last = "Preston-Werner" } 1727 point = { x = 1, y = 2 } 1728 animal = { type.name = "pug" } 1729 `); 1730 assert(doc["name"]["first"] == "Tom"); 1731 assert(doc["name"]["last"] == "Preston-Werner"); 1732 assert(doc["point"] == ["x" : 1, "y" : 2]); 1733 assert(doc["animal"]["type"]["name"] == "pug"); 1734 1735 // --------------- 1736 // Array of Tables 1737 // --------------- 1738 1739 doc = parseTOML(` 1740 [[products]] 1741 name = "Hammer" 1742 sku = 738594937 1743 1744 [[products]] 1745 1746 [[products]] 1747 name = "Nail" 1748 sku = 284758393 1749 color = "gray" 1750 `); 1751 assert(doc["products"].type == TOML_TYPE.ARRAY); 1752 assert(doc["products"].array.length == 3); 1753 assert(doc["products"][0] == ["name" : TOMLValue("Hammer"), "sku" : TOMLValue(738594937)]); 1754 assert(doc["products"][1] == (TOMLValue[string]).init); 1755 assert(doc["products"][2] == ["name" : TOMLValue("Nail"), "sku" 1756 : TOMLValue(284758393), "color" : TOMLValue("gray")]); 1757 1758 // nested 1759 doc = parseTOML(` 1760 [[fruit]] 1761 name = "apple" 1762 1763 [fruit.physical] 1764 color = "red" 1765 shape = "round" 1766 1767 [[fruit.variety]] 1768 name = "red delicious" 1769 1770 [[fruit.variety]] 1771 name = "granny smith" 1772 1773 [[fruit]] 1774 name = "banana" 1775 1776 [[fruit.variety]] 1777 name = "plantain" 1778 `); 1779 assert(doc["fruit"].type == TOML_TYPE.ARRAY); 1780 assert(doc["fruit"].array.length == 2); 1781 assert(doc["fruit"][0]["name"] == "apple"); 1782 assert(doc["fruit"][0]["physical"] == ["color" : "red", "shape" : "round"]); 1783 assert(doc["fruit"][0]["variety"][0] == ["name" : "red delicious"]); 1784 assert(doc["fruit"][0]["variety"][1]["name"] == "granny smith"); 1785 assert(doc["fruit"][1] == ["name" : TOMLValue("banana"), "variety" 1786 : TOMLValue([["name" : "plantain"]])]); 1787 1788 assertThrown!TOMLException({ parseTOML(` 1789 # INVALID TOML DOC 1790 [[fruit]] 1791 name = "apple" 1792 1793 [[fruit.variety]] 1794 name = "red delicious" 1795 1796 # This table conflicts with the previous table 1797 [fruit.variety] 1798 name = "granny smith" 1799 `); }()); 1800 1801 doc = parseTOML(` 1802 points = [ { x = 1, y = 2, z = 3 }, 1803 { x = 7, y = 8, z = 9 }, 1804 { x = 2, y = 4, z = 8 } ] 1805 `); 1806 assert(doc["points"].array.length == 3); 1807 assert(doc["points"][0] == ["x" : 1, "y" : 2, "z" : 3]); 1808 assert(doc["points"][1] == ["x" : 7, "y" : 8, "z" : 9]); 1809 assert(doc["points"][2] == ["x" : 2, "y" : 4, "z" : 8]); 1810 1811 // additional tests for code coverage 1812 1813 assert(TOMLValue(42) == 42.0); 1814 assert(TOMLValue(42) != "42"); 1815 assert(TOMLValue("42") != 42); 1816 1817 try 1818 { 1819 parseTOML(` 1820 1821 error = @ 1822 `); 1823 } 1824 catch (TOMLParserException e) 1825 { 1826 assert(e.position.line == 3); // start from line 1 1827 assert(e.position.column == 9 + 3); // 3 tabs 1828 } 1829 1830 assertThrown!TOMLException({ parseTOML(`error = "unterminated`); }()); 1831 assertThrown!TOMLException({ parseTOML(`error = 'unterminated`); }()); 1832 assertThrown!TOMLException({ parseTOML(`error = "\ "`); }()); 1833 1834 assertThrown!TOMLException({ parseTOML(`error = truè`); }()); 1835 assertThrown!TOMLException({ parseTOML(`error = falsè`); }()); 1836 1837 assertThrown!TOMLException({ parseTOML(`[error`); }()); 1838 1839 doc = parseTOML(`test = "\\\"\b\t\n\f\r\u0040\U00000040"`); 1840 assert(doc["test"] == "\\\"\b\t\n\f\r@@"); 1841 1842 doc = parseTOML(`test = """quoted "string"!"""`); 1843 assert(doc["test"] == "quoted \"string\"!"); 1844 1845 // options 1846 1847 assert(parseTOML(`raw = this is unquoted`, 1848 TOMLOptions.unquotedStrings)["raw"] == "this is unquoted"); 1849 1850 // document 1851 1852 TOMLValue value = TOMLValue(["test" : 44]); 1853 doc = TOMLDocument(value); 1854 1855 // opEquals 1856 1857 assert(TOMLValue(true) == TOMLValue(true)); 1858 assert(TOMLValue("string") == TOMLValue("string")); 1859 assert(TOMLValue(0) == TOMLValue(0)); 1860 assert(TOMLValue(.0) == TOMLValue(.0)); 1861 assert(TOMLValue(SysTime.fromISOExtString("1979-05-27T00:32:00-07:00")) == TOMLValue( 1862 SysTime.fromISOExtString("1979-05-27T00:32:00-07:00"))); 1863 assert(TOMLValue(DateTime.fromISOExtString("1979-05-27T07:32:00")) == TOMLValue( 1864 DateTime.fromISOExtString("1979-05-27T07:32:00"))); 1865 assert(TOMLValue(Date.fromISOExtString("1979-05-27")) == TOMLValue( 1866 Date.fromISOExtString("1979-05-27"))); 1867 assert(TOMLValue(TimeOfDay.fromISOExtString("07:32:00")) == TOMLValue( 1868 TimeOfDay.fromISOExtString("07:32:00"))); 1869 assert(TOMLValue([1, 2, 3]) == TOMLValue([1, 2, 3])); 1870 assert(TOMLValue(["a" : 0, "b" : 1]) == TOMLValue(["a" : 0, "b" : 1])); 1871 1872 // toString() 1873 1874 assert(TOMLDocument(["test" : TOMLValue(0)]).toString() == "test = 0" ~ newline); 1875 1876 assert(TOMLValue(true).toString() == "true"); 1877 assert(TOMLValue("string").toString() == "\"string\""); 1878 assert(TOMLValue("\"quoted \\ \b \f \r\n \t string\"") 1879 .toString() == "\"\\\"quoted \\\\ \\b \\f \\r\\n \\t string\\\"\""); 1880 assert(TOMLValue(42).toString() == "42"); 1881 assert(TOMLValue(99.44).toString() == "99.44"); 1882 assert(TOMLValue(.0).toString() == "0.0"); 1883 assert(TOMLValue(1e100).toString() == "1e+100"); 1884 assert(TOMLValue(SysTime.fromISOExtString("1979-05-27T00:32:00-07:00")) 1885 .toString() == "1979-05-27T00:32:00-07:00"); 1886 assert(TOMLValue(DateTime.fromISOExtString("1979-05-27T07:32:00")) 1887 .toString() == "1979-05-27T07:32:00"); 1888 assert(TOMLValue(Date.fromISOExtString("1979-05-27")).toString() == "1979-05-27"); 1889 assert(TOMLValue(TimeOfDay.fromISOExtString("07:32:00.999999")).toString() == "07:32:00.999999"); 1890 assert(TOMLValue([1, 2, 3]).toString() == "[1, 2, 3]"); 1891 immutable table = TOMLValue(["a" : 0, "b" : 1]).toString(); 1892 assert(table == "{ a = 0, b = 1 }" || table == "{ b = 1, a = 0 }"); 1893 1894 foreach (key, value; TOMLValue(["0" : 0, "1" : 1])) 1895 { 1896 assert(value == key.to!int); 1897 } 1898 1899 value = 42; 1900 assert(value.type == TOML_TYPE.INTEGER); 1901 assert(value == 42); 1902 value = TOMLValue("42"); 1903 assert(value.type == TOML_TYPE.STRING); 1904 assert(value == "42"); 1905 1906 } 1907 1908 /** 1909 * Exception thrown on generic TOML errors. 1910 */ 1911 class TOMLException : Exception 1912 { 1913 1914 public this(string message, string file = __FILE__, size_t line = __LINE__) 1915 { 1916 super(message, file, line); 1917 } 1918 1919 } 1920 1921 /** 1922 * Exception thrown during the parsing of TOML document. 1923 */ 1924 class TOMLParserException : TOMLException 1925 { 1926 1927 private Tuple!(size_t, "line", size_t, "column") _position; 1928 1929 public this(string message, size_t line, size_t column, string file = __FILE__, 1930 size_t _line = __LINE__) 1931 { 1932 super(message ~ " (" ~ to!string(line) ~ ":" ~ to!string(column) ~ ")", file, _line); 1933 this._position.line = line; 1934 this._position.column = column; 1935 } 1936 1937 /** 1938 * Gets the position (line and column) where the parsing expection 1939 * has occured. 1940 */ 1941 public pure nothrow @property @safe @nogc auto position() 1942 { 1943 return this._position; 1944 } 1945 1946 }