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