1 /// A simple JSON pull parser written according to RFC 8259. 2 module rcdata.json; 3 4 import std.range; 5 import std.traits; 6 import std.format; 7 import std.algorithm; 8 import std.exception; 9 10 import std.conv : to; 11 12 import rcdata.utils; 13 14 /// Struct for parsing JSON. 15 struct JSONParser { 16 17 /// Type of a JSON value 18 enum Type { 19 20 null_, 21 boolean, 22 number, 23 string, 24 array, 25 object 26 27 } 28 29 /// Input taken by the parser. 30 ForwardRange!dchar input; 31 32 /// Current line number. 33 size_t lineNumber = 1; 34 35 @disable this(); 36 37 /// Start parsing using the given object, converting it to an InputRange. 38 this(T)(T input) 39 if (isForwardRange!T) { 40 41 this.input = input.inputRangeObject; 42 43 } 44 45 /// Check the next type in the document. 46 /// Returns: Type of the object. 47 Type peekType() { 48 49 skipSpace(); 50 51 // Nothing left 52 if (input.empty) { 53 54 throw new JSONException("Unexpected end of file."); 55 56 } 57 58 with (Type) 59 switch (input.front) { 60 61 // Valid types 62 case 'n': return null_; 63 64 case 't': 65 case 'f': return boolean; 66 67 case '-': 68 case '0': 69 .. 70 case '9': return number; 71 72 case '"': return string; 73 74 case '[': return array; 75 case '{': return object; 76 77 // Errors 78 case ']': 79 case '}': 80 81 throw new JSONException( 82 failMsg(input.front.format!"Unexpected '%s' (maybe there's a comma before it?)") 83 ); 84 85 // Other errors 86 default: 87 88 throw new JSONException( 89 failMsg(input.front.format!"Unexpected character '%s'") 90 ); 91 92 } 93 94 } 95 96 // TODO: switchType 97 98 /// Get a value of the matching type. 99 /// Params: 100 /// T = Built-in type expected to be returned, or an element of the `Type` enum. 101 template get(T) { 102 103 enum isTypeEnum(Type type) = is(typeof(T) == Type) && T == type; 104 105 // Boolean 106 static if (is(T : bool) || isTypeEnum!(Type.boolean)) { 107 alias get = getBoolean; 108 } 109 110 // Enum 111 else static if (is(T == enum) && isNumeric!T) { 112 alias get = getEnum!T; 113 } 114 115 // Number 1 116 else static if (isNumeric!T) { 117 alias get = getNumber!T; 118 } 119 120 // Number 2 121 else static if (isTypeEnum!(Type.number)) { 122 alias get = getNumber!float; 123 } 124 125 // String 126 else static if (isSomeString!T) { 127 T get() { 128 return getString.to!T; 129 } 130 } 131 132 // String 2 133 else static if (isTypeEnum!(Type..string)) { 134 alias get = getString; 135 } 136 137 // Arrays 138 else static if (is(T == U[], U)) { 139 alias get = getArray!U; 140 } 141 142 // Static arrays 143 else static if (is(T == U[N], U, size_t N)) { 144 alias get = getArray!T; 145 } 146 147 // Associative arrays 148 else static if (is(T == U[wstring], U)) { 149 alias get = getAssoc!U; 150 } 151 152 else static if (is(T == U[Y], U, Y) && isSomeString!Y) { 153 T get() { 154 return getAssoc!U.to!T; 155 } 156 } 157 158 // Objects 159 else static if (is(T == struct) || is(T == class)) { 160 alias get = getStruct!T; 161 } 162 163 else static assert(0, fullyQualifiedName!T.format!"Type %s is currently unsupported by get()"); 164 165 } 166 167 /// Skip the next value in the JSON. 168 /// Throws: `JSONException` on syntax error. 169 void skipValue() { 170 171 const nextType = peekType(); 172 final switch (nextType) { 173 174 case Type.null_: 175 getNull(); 176 break; 177 178 case Type.boolean: 179 getBoolean(); 180 break; 181 182 case Type.number: 183 getNumber!string; 184 break; 185 186 case Type..string: 187 getString(); 188 break; 189 190 case Type.array: 191 192 // Skip all values 193 foreach (index; getArray) skipValue(); 194 break; 195 196 case Type.object: 197 198 // Skip all values 199 foreach (key; getObject) skipValue(); 200 break; 201 202 } 203 204 } 205 206 /// Expect the next value to be null and skip to the next value. 207 /// 208 /// Despite the name, this function doesn't return. 209 /// 210 /// Throws: `JSONException` if the next item isn't a null. 211 void getNull() { 212 213 skipSpace(); 214 215 // Check the values 216 enforce!JSONException(input.skipOver("null"), failFoundMsg("Expected null")); 217 218 } 219 220 /// Get a boolean and skip to the next value. 221 /// Throws: `JSONException` if the next item isn't a boolean. 222 /// Returns: The parsed boolean. 223 bool getBoolean() { 224 225 skipSpace(); 226 227 // Check the values 228 if (input.skipOver("true")) return true; 229 else if (input.skipOver("false")) return false; 230 231 // Or fail 232 else throw new JSONException(failFoundMsg("Expected boolean")); 233 234 } 235 236 /// Load a numeric enum; expects either a string or an int value. 237 T getEnum(T)() 238 if (isNumeric!T && is(T == enum)) { 239 240 auto type = peekType; 241 242 if (type == Type..string) { 243 244 return getString.to!T; 245 246 } 247 248 else if (type == Type.number) { 249 250 return getNumber!T; 251 252 } 253 254 else throw new JSONException(failFoundMsg("Expected enum value (string or int)")); 255 256 } 257 258 // Loading enums 259 unittest { 260 261 enum MyEnum { 262 maybe, 263 no, 264 yes, 265 } 266 267 auto json = JSONParser(q{ ["maybe", 1, 2, "yes"] }); 268 assert(json.getArray!MyEnum == [MyEnum.maybe, MyEnum.no, MyEnum.yes, MyEnum.yes]); 269 270 } 271 272 /// Get the next number. 273 /// 274 /// The number will be verified according to the JSON spec, but is parsed using std. Because of this, you can 275 /// request a string return value, in order to perform manual conversion if needed. 276 /// 277 /// Implementation note: If the number contains an uppercase E, it will be converted to lowercase. 278 /// 279 /// Params: 280 /// T = Type of the returned number, eg. `int` or `real`. Can also be a `string` type, if conversion should be 281 /// done manually, or the number is expected to be big. 282 /// Throws: `JSONException` if the next item isn't a number. 283 /// Returns: The matched number. 284 T getNumber(T)() 285 if (isNumeric!T || isSomeString!T) { 286 287 skipSpace(); 288 289 // Match the string 290 dstring number = input.skipOver("-") ? "-" : ""; 291 292 /// Push the current character, plus following digits, to the result string. 293 /// Returns: Length of the matched string. 294 size_t pushDigit() { 295 296 size_t length; 297 298 do { 299 number ~= input.front; 300 input.popFront; 301 length++; 302 } 303 while (!input.empty && '0' <= input.front && input.front <= '9'); 304 305 return length; 306 307 } 308 309 // Check the first digit 310 enforce!JSONException(!input.empty && '0' <= input.front && input.front <= '9', 311 failFoundMsg("Expected number")); 312 313 // Parse integer part 314 const leadingZero = input.front == '0'; 315 const digits = pushDigit(); 316 317 // Check for leading zeros 318 enforce!JSONException(!leadingZero || digits == 1, 319 failMsg("Numbers cannot have leading zeros, found")); 320 321 // Fractal part 322 if (!input.empty && input.front == '.') pushDigit(); 323 324 // Exponent 325 if (!input.empty && (input.front == 'e' || input.front == 'E')) { 326 327 // Add the E 328 number ~= 'e'; 329 input.popFront; 330 331 // EOF? 332 enforce!JSONException(!input.empty, "Unexpected EOF in exponent"); 333 334 // Check for sign 335 if (input.front == '-' || input.front == '+') { 336 number ~= input.front; 337 input.popFront; 338 } 339 340 // Push the numbers 341 // RFC 8259 actually allows leading zeros here 342 enforce!JSONException( 343 '0' <= input.front && input.front <= '9', 344 failMsg(input.front.format!"Unexpected character '%s' in exponent") 345 ); 346 347 // Push the digits 348 pushDigit(); 349 350 } 351 352 // Enum 353 static if (is(T == enum)) { 354 return number.to!(OriginalType!T).to!T; 355 } 356 357 // Number 358 else return number.to!T; 359 360 } 361 362 /// Get the next string. 363 /// Throws: `JSONException` if the next item isn't a string. 364 /// Returns: The matched string in UTF-16, because JSON uses it to encode strings. 365 wstring getString() { 366 367 skipSpace(); 368 369 wstring result; 370 size_t startLine = lineNumber; 371 372 // Require a quotation mark 373 enforce!JSONException(input.skipOver(`"`), failFoundMsg("Expected string")); 374 375 // Read next characters 376 loop: while (true) { 377 378 enforce!JSONException(!input.empty, startLine.format!"Unclosed string starting at line %s"); 379 380 // Don't accept control codes 381 enforce!JSONException(input.front != 10, 382 failMsg("JSON strings cannot contain line feeds, use \n instead.")); 383 enforce!JSONException(input.front >= 20, 384 failMsg("Illegal control point in a string, use an escape code instead")); 385 386 switch (input.front) { 387 388 // Closing the string 389 case '"': 390 391 input.popFront; 392 break loop; 393 394 // Escape code 395 case '\\': 396 397 result ~= getEscape(); 398 break; 399 400 // Other characters 401 default: 402 403 result ~= input.front; 404 input.popFront(); 405 406 } 407 408 } 409 410 return result; 411 412 } 413 414 /// Get array elements by iterating over them. 415 /// 416 /// Note: You must read exactly one array item per iteration, otherwise the generator will crash. 417 /// 418 /// Throws: `JSONException` if the next item isn't an array or there's a syntax error. 419 /// Returns: A generator range yielding current array index until all the items are read. 420 auto getArray() { 421 422 import std.concurrency : Generator, yield; 423 424 skipSpace(); 425 426 // Expect an array opening 427 enforce!JSONException(input.skipOver("["), failFoundMsg("Expected an array")); 428 429 return new Generator!size_t({ 430 431 size_t index; 432 433 // Skip over space 434 skipSpace(); 435 436 // Check the contents 437 while (!input.skipOver("]")) { 438 439 // Require a comma after non-zero indexes 440 enforce!JSONException( 441 !index || input.skipOver(","), 442 failFoundMsg("Expected a comma between array elements") 443 ); 444 445 // Expect an item 446 yield(index++); 447 448 skipSpace(); 449 450 } 451 452 }); 453 454 } 455 456 /// Get an array of elements matching the type. 457 /// 458 /// Throws: `JSONException` if there's a type mismatch or syntax error. 459 T[] getArray(T)() { 460 461 T[] result; 462 foreach (index; getArray) { 463 464 result ~= get!T; 465 466 } 467 468 return result; 469 470 } 471 472 /// 473 unittest { 474 475 auto json = JSONParser(q{ ["test", "foo", "bar"] }); 476 assert(json.getArray!string == ["test", "foo", "bar"]); 477 478 } 479 480 /// Get a static array of elements matching the type. 481 /// Throws: `JSONException` if there's a type mismatch or syntax error. 482 T getArray(T : Element[Size], Element, size_t Size)() { 483 484 T result; 485 foreach (index; getArray) { 486 487 result[index] = get!Element; 488 489 } 490 return result; 491 492 } 493 494 /// 495 unittest { 496 497 auto text = q{ [1, 2, 3] }; 498 499 { 500 auto json = JSONParser(text); 501 auto values = json.getArray!(uint[3]); 502 503 static assert(is(typeof(values) == uint[3])); 504 assert(values == [1, 2, 3]); 505 } 506 507 { 508 auto json = JSONParser(text); 509 auto values = json.getArray!(uint, 3); 510 511 static assert(is(typeof(values) == uint[3])); 512 assert(values == [1, 2, 3]); 513 514 } 515 516 } 517 518 /// Get a static array of elements matching the types. 519 /// Throws: `JSONException` if there's a type mismatch or syntax error. 520 Element[Size] getArray(Element, size_t Size)() { 521 522 return getArray!(Element[Size]); 523 524 } 525 526 /// Get an associative array from the JSON. 527 /// Throws: `JSONException` on type mismatch or syntax error. 528 /// Returns: The requested associative array. 529 T[wstring] getAssoc(T)() { 530 531 T[wstring] result; 532 foreach (key; getObject) { 533 534 result[key] = get!T; 535 536 } 537 return result; 538 539 } 540 541 /// 542 unittest { 543 544 auto json = JSONParser(q{ 545 { 546 "hello": 123, 547 "foo": -123, 548 "test": 42.123 549 } 550 }); 551 552 auto assoc = json.getAssoc!float; 553 554 assert(assoc["hello"] == 123); 555 assert(assoc["foo"] == -123); 556 assert(assoc["test"] == 42.123f); 557 558 } 559 560 /// Get object contents by iterating over them. 561 /// 562 /// Note: You must read exactly one item per key, otherwise the generator will crash. 563 /// 564 /// Throws: `JSONException` on type mismatch or syntax error. 565 /// Returns: A generator yielding the found key, in document order. 566 auto getObject() { 567 568 import std.concurrency : Generator, yield; 569 570 skipSpace(); 571 572 // Expect an array opening 573 enforce!JSONException(input.skipOver("{"), failFoundMsg("Expected an object")); 574 575 return new Generator!wstring({ 576 577 skipSpace(); 578 579 bool first = true; 580 581 // Check the contents 582 while (!input.skipOver("}")) { 583 584 // If this isn't the first item 585 if (!first) { 586 587 // Require a comma 588 enforce!JSONException(input.skipOver(","), failFoundMsg("Expected a comma between object items")); 589 590 } 591 else first = false; 592 593 // Read the key 594 auto key = getString(); 595 596 // Expect a colon 597 skipSpace(); 598 enforce!JSONException(input.skipOver(":"), failFoundMsg("Expected a colon after object key")); 599 600 // Pass the key to the item 601 yield(key); 602 603 // Skip space 604 skipSpace(); 605 606 } 607 608 }); 609 610 } 611 612 /// Load given struct or class from JSON. 613 /// 614 /// If the object has a `fromJSON` method, it will be called to load the data. Otherwise, an attempt will be made to 615 /// read a JSON object and translate its properties to fields of the struct or class. The object doesn't have to 616 /// provide all fields defined in the struct or class. 617 /// 618 /// JSON fields that share names with D reserved keywords can be suffixed with `_` in code, per the 619 /// $(LINK2 https://dlang.org/dstyle.html#naming_keywords, D style specification). Alternatively, 620 /// `@JSONName("keyword")` can be used to pick a different name to use for the JSON key. 621 /// 622 /// For `getStruct`, the struct or class must have a default constructor available. 623 /// 624 /// Inaccessible (eg. private or protected in external access) fields and fields marked with `@JSONExclude` will not 625 /// be affected. 626 /// 627 /// Throws: `JSONException` if there's a type mismatch or syntax error. 628 /// Params: 629 /// T = Type of the struct. 630 /// obj = Instance of the object to modify. Classes are edited in place, structs are not. 631 /// fallback = Function to call if a field doesn't exist. By default, such fields are ignored. The callback is 632 /// not taken into account if the struct defines `fromJSON`. 633 /// Returns: 634 /// 1. If T is a struct, a copy of the given object with updated properties. 635 /// 2. If T is a class, a reference to the given object. (ret is obj) 636 T updateStruct(T)(return scope T obj, void delegate(ref T, wstring) fallback = null) 637 if (is(T == struct) || is(T == class)) { 638 639 /// If the object has a fromJSON property, use it instead. 640 static if (__traits(hasMember, obj, "fromJSON")) { 641 642 obj.fromJSON(this); 643 644 } 645 646 // Expect an object 647 else foreach (key; getObject) { 648 649 import std.string : chomp; 650 import std.meta : AliasSeq, staticMap; 651 652 // Check parents 653 static if (is(T == class)) { 654 alias FullT = AliasSeq!(BaseClassesTuple!T, T); 655 } 656 else { 657 alias FullT = T; 658 } 659 660 alias FieldTypes = staticMap!(Fields, FullT); 661 662 // Match struct fields 663 fields: switch (key.to!string) { 664 665 static foreach (i, field; staticMap!(FieldNameTuple, FullT)) {{ 666 667 // Check if the field is accessible (private/JSONExclude) 668 enum compatible = __traits(compiles, mixin("T." ~ field)) 669 && !hasUDA!(mixin("T." ~ field), JSONExclude); 670 671 // Find the UDA name 672 alias name = getUDAs!(mixin("T." ~ field), JSONName); 673 674 static assert(name.length <= 1); 675 676 // Pick field name based on UDA 677 static if (name.length == 0) 678 enum fieldName = field.chomp("_"); 679 else 680 enum fieldName = name[0].originalName; 681 682 alias FieldType = FieldTypes[i]; 683 684 // Load the value 685 static if (compatible) { 686 687 case fieldName: 688 689 // Skip null values 690 if (peekType == Type.null_) skipValue(); 691 692 // Read the value 693 else __traits(getMember, obj, field) = get!FieldType; 694 695 break fields; 696 697 } 698 699 }} 700 701 default: 702 703 // If the fallback isn't null, call it 704 if (fallback !is null) fallback(obj, key); 705 706 // Otherwise just skip the value 707 else skipValue(); 708 709 } 710 711 } 712 713 return obj; 714 715 } 716 717 /// Ditto 718 T getStruct(T)(void delegate(ref T, wstring) fallback = null) { 719 720 // Create the object 721 static if (is(T == struct)) { 722 auto obj = T(); 723 } 724 else { 725 auto obj = new T(); 726 } 727 728 return updateStruct!T(obj, fallback); 729 730 } 731 732 /// 733 unittest { 734 735 struct Example { 736 string name; 737 int version_; 738 string[] contents; 739 } 740 741 auto json = JSONParser(q{ 742 { 743 "name": "rcjson", 744 "version": 123, 745 "contents": ["json-parser"] 746 } 747 }); 748 const obj = json.getStruct!Example; 749 assert(obj.name == "rcjson"); 750 assert(obj.version_ == 123); 751 assert(obj.contents == ["json-parser"]); 752 753 } 754 755 /// Using fallback 756 unittest { 757 758 struct Table { 759 760 @JSONName("table-name") 761 string tableName; 762 763 @JSONExclude 764 string[string] attributes; 765 766 } 767 768 auto json = JSONParser(q{ 769 { 770 "table-name": "Player", 771 "id": "PRIMARY KEY INT", 772 "name": "VARCHAR(30)", 773 "xp": "INT", 774 "attributes": "VARCHAR(60)" 775 } 776 }); 777 778 auto table = json.getStruct!Table((ref Table table, wstring key) { 779 780 table.attributes[key.to!string] = json.getString.to!string; 781 782 }); 783 784 assert(table.tableName == "Player"); 785 assert(table.attributes["id"] == "PRIMARY KEY INT"); 786 assert(table.attributes["xp"] == "INT"); 787 assert(table.attributes["attributes"] == "VARCHAR(60)"); 788 789 } 790 791 /// Using `fromJSON` 792 unittest { 793 794 static struct Snowflake { 795 796 ulong id; 797 798 alias id this; 799 800 void fromJSON(JSONParser parser) { 801 802 id = parser.get!string.to!ulong; 803 804 } 805 806 } 807 808 auto json = JSONParser(`"1234567890"`); 809 assert(json.get!Snowflake == 1234567890); 810 811 } 812 813 /// Copy the parser. Useful to keep document data for later. 814 JSONParser save() { 815 816 return this; 817 818 } 819 820 /// Saving parser state 821 unittest { 822 auto json = JSONParser(q{ 823 [ 824 { 825 "name": "A", 826 "ability": "doFoo", 827 "health": 30 828 }, 829 { 830 "name": "B", 831 "ability": "doBar", 832 "health": null 833 }, 834 { 835 "name": "C", 836 "inherits": ["A", "B"], 837 "ability": "doTest" 838 } 839 ] 840 }); 841 842 static class EntityMeta { 843 string name; 844 string[] inherits; 845 } 846 847 static class Entity : EntityMeta { 848 string ability; 849 int health = 100; 850 } 851 852 JSONParser[string] states; 853 Entity[string] entities; 854 foreach (index; json.getArray) { 855 856 // Get the metadata and save the state 857 auto state = json.save; 858 auto meta = json.getStruct!EntityMeta; // Efficient and quick way to fetch the two attributes 859 states[meta.name] = state.save; 860 861 // Create the object 862 auto entity = new Entity(); 863 864 // Inherit properties 865 foreach (parent; meta.inherits) { 866 867 // Get the parent 868 auto parentState = states[parent].save; 869 870 // Inherit its values 871 parentState.updateStruct(entity); 872 873 } 874 875 // Now, add local data 876 state.updateStruct(entity); 877 878 entities[entity.name] = entity; 879 880 } 881 882 const a = entities["A"]; 883 assert(a.name == "A"); 884 assert(a.ability == "doFoo"); 885 assert(a.health == 30); 886 887 const b = entities["B"]; 888 assert(b.name == "B"); 889 assert(b.ability == "doBar"); 890 assert(b.health == 100); 891 892 const c = entities["C"]; 893 assert(c.name == "C"); 894 assert(c.ability == "doTest"); 895 assert(c.health == 30); // A had explicitly stated health, B did not — inherited from A. 896 // Otherwise impossible without saving states. 897 898 } 899 900 /// Parse the next escape code in the JSON. 901 /// Returns: The escaped character. 902 private wchar getEscape() { 903 904 assert(!input.empty, "getEscape called with empty input"); 905 assert(input.front == '\\', "getEscape called, but no escape code was found"); 906 907 // Pop the backslash 908 input.popFront(); 909 910 // Message to throw in case of failure 911 string eofError() { return failMsg("Reached end of file in the middle of an escape code"); } 912 913 enforce!JSONException(!input.empty, eofError); 914 915 // Match the first character of the escape code 916 const ch = input.front; 917 input.popFront(); 918 919 switch (ch) { 920 921 // Obvious escape codes 922 case '"', '\\', '/': return cast(wchar) ch; 923 924 // Special 925 case 'b': return '\b'; 926 case 'f': return '\f'; 927 case 'n': return '\n'; 928 case 'r': return '\r'; 929 case 't': return '\t'; 930 case 'u': 931 932 // Take next 4 characters 933 auto code = input.take(4).to!string; 934 935 // Must be 4 characters 936 enforce!JSONException(code.length == 4, eofError); 937 938 // Now, create the character 939 return code.to!ushort(16); 940 941 default: 942 943 throw new JSONException( 944 failMsg(ch.format!"Unknown escape code '\\%s'") 945 ); 946 947 948 } 949 950 } 951 952 /// Skips over line breaks and advances line count. 953 /// Returns: Matched line breaks. 954 private string getLineBreaks() { 955 956 import std.stdio : writeln; 957 958 string match = ""; 959 960 /// Last matched separator 961 dchar lineSep; 962 963 loop: while (!input.empty) 964 switch (input.front) { 965 966 case '\n', '\r': 967 968 // Match the next character 969 match ~= input.front; 970 971 // Using the same separator, or this is the first one 972 if (lineSep == input.front || lineSep == dchar.init) { 973 974 // Advance line count 975 lineNumber++; 976 977 } 978 979 // Encountered a different one? Most likely CRLF, so we shouldn't count the LF. 980 981 // Update the lineSep char 982 lineSep = input.front; 983 input.popFront(); 984 985 // Continue parsing 986 continue; 987 988 default: break loop; 989 990 } 991 992 // Return the match 993 return match; 994 995 } 996 997 /// Skip whitespace in the document. 998 private void skipSpace() { 999 1000 // RFC: See section 2. 1001 1002 // Skip an indefinite amount 1003 while (!input.empty) 1004 switch (input.front) { 1005 1006 // Line feed 1007 case '\n', '\r': 1008 1009 // Skip over 1010 getLineBreaks(); 1011 continue; 1012 1013 // Remove whitespace 1014 case ' ', '\t': 1015 input.popFront(); 1016 continue; 1017 1018 // Stop on anything else 1019 default: 1020 return; 1021 1022 } 1023 1024 } 1025 1026 /// Fail with given message and include a line number. 1027 private string failMsg(string msg) { 1028 1029 return msg.format!"%s on line %s"(lineNumber); 1030 1031 } 1032 1033 /// Fail with the given message and output the given message, including the next word in the input range. 1034 pragma(inline, true); 1035 private string failFoundMsg(string msg) { 1036 1037 skipSpace(); 1038 1039 return failMsg(msg.format!"%s, found %s"(peekType)); 1040 1041 } 1042 1043 } 1044 1045 unittest { 1046 1047 auto json = JSONParser(q{ 1048 [ 1049 "hello", 1050 "world", 1051 true, 1052 123, 1053 { 1054 "undefined": null, 1055 "int": 123, 1056 "negative": -123, 1057 "float": 123.213 1058 } 1059 ] 1060 }); 1061 1062 // Type validation 1063 assert(json.getBoolean.collectExceptionMsg == "Expected boolean, found array on line 2"); 1064 1065 // Checking types early 1066 assert(json.peekType == JSONParser.Type.array); 1067 1068 // Now, let's get into the contents of the array 1069 foreach (index; json.getArray) { 1070 1071 with (JSONParser.Type) 1072 switch (json.peekType) { 1073 1074 case string: 1075 1076 // We have two strings, at indexes 0 and 1 1077 if (index == 0) assert(json.getString == "hello"); 1078 if (index == 1) assert(json.getString == "world"); 1079 break; 1080 1081 case boolean: 1082 1083 // The only boolean in our array is "true" 1084 assert(json.getBoolean); 1085 break; 1086 1087 case number: 1088 1089 // Now we've got a number 1090 assert(json.getNumber!int == 123); 1091 break; 1092 1093 case object: 1094 1095 wstring[] keys; 1096 1097 // Iterate over object items 1098 foreach (key; json.getObject) { 1099 1100 if (key == "undefined") json.getNull(); 1101 else if (key == "int") assert(json.getNumber!int == 123); 1102 else json.skipValue(); 1103 1104 keys ~= key; 1105 1106 } 1107 1108 // Checked the keys, all in order 1109 assert(keys == ["undefined"w, "int"w, "negative"w, "float"w]); 1110 1111 break; 1112 1113 default: 1114 1115 assert(0); 1116 1117 } 1118 1119 } 1120 1121 } 1122 1123 /// 1124 unittest { 1125 1126 auto json = JSONParser(q{ 1127 [ 1128 { 1129 "name": "John", 1130 "surname": "Doe", 1131 "age": 42 1132 }, 1133 { 1134 "name": "Jane", 1135 "surname": "Doe", 1136 "age": 46 1137 } 1138 ] 1139 }); 1140 1141 // Check each array item 1142 foreach (index; json.getArray) { 1143 1144 // Read the object 1145 auto keys = json.getObject; 1146 1147 // Check the name 1148 assert(keys.front == "name"); 1149 json.skipValue(); 1150 keys.popFront(); 1151 1152 // Surname 1153 assert(keys.front == "surname"); 1154 assert(json.getString == "Doe"); 1155 keys.popFront(); 1156 1157 // Age 1158 assert(keys.front == "age"); 1159 assert(json.getNumber!uint > 40); 1160 keys.popFront(); 1161 1162 // Done 1163 assert(keys.empty); 1164 1165 } 1166 1167 } 1168 1169 /// Moving to struct with a helper 1170 unittest { 1171 1172 struct Person { 1173 1174 string name; 1175 string surname; 1176 uint age; 1177 1178 } 1179 1180 auto json = JSONParser(q{ 1181 [ 1182 { 1183 "name": "John", 1184 "surname": "Doe", 1185 "age": 42 1186 }, 1187 { 1188 "name": "Jane", 1189 "surname": "Doe", 1190 "age": 46 1191 } 1192 ] 1193 }); 1194 1195 auto people = json.getArray!Person; 1196 1197 assert(people[0].name == "John"); 1198 assert(people[1].name == "Jane"); 1199 assert(people[0].age == 42); 1200 assert(people[1].age == 46); 1201 1202 } 1203 1204 unittest { 1205 1206 foreach (num; [ 1207 "0", 1208 "123", 1209 "123.123", 1210 "-123", 1211 "-3", 1212 "-3.123", 1213 "0.123e2", 1214 "0.123e-2", 1215 "0.123e-2", 1216 "0.0123e-2", 1217 ]) { 1218 1219 import std.string : toLower; 1220 1221 auto res1 = JSONParser(num).getNumber!real; 1222 assert(res1 == num.to!real, format!`Number "%s" is parsed into a wrong number value, "%s"`(num, res1)); 1223 1224 auto res2 = JSONParser(num).getNumber!string; 1225 assert(res2 == num.toLower, format!`Number "%s" changes string value to "%s"`(num, res2)); 1226 1227 } 1228 1229 // Invalid cases 1230 foreach (num; [ 1231 "0123", 1232 "+123", 1233 "- 123", 1234 // Those will not fail instantly, requie checking next value 1235 // "123e123.123" 1236 // "123 123" 1237 ]) { 1238 1239 assertThrown(JSONParser(num).getNumber!string, 1240 num.format!`Number "%s" is invalid, but doesn't throw when parsed`); 1241 1242 } 1243 1244 } 1245 1246 unittest { 1247 1248 import std.array : array; 1249 1250 auto json = JSONParser("[false true]"); 1251 1252 assert( 1253 json.getArray.map!(i => json.getBoolean).array.collectExceptionMsg 1254 == "Expected a comma between array elements, found boolean on line 1" 1255 ); 1256 1257 } 1258 1259 unittest { 1260 1261 auto text = q{ 1262 123 "hello, world" 123.124 1263 }; 1264 1265 auto jsonA = JSONParser(text); 1266 auto jsonB = JSONParser(text); 1267 assert(jsonA.get!int == jsonB.getNumber!int); 1268 assert(jsonA.get!wstring == jsonB.getString); 1269 assert(jsonA.get!float == jsonB.getNumber!float); 1270 1271 1272 1273 } 1274 1275 unittest { 1276 1277 auto json1 = JSONParser(`"\uD834\uDD1E"`); 1278 assert(json1.getString == "\U0001D11E"); 1279 1280 import std.stdio : writefln; 1281 auto json2 = JSONParser(`"\u0020\u000A\n\t"`); 1282 assert(json2.getString == " \n\n\t"); 1283 1284 } 1285 1286 unittest { 1287 1288 struct A { 1289 1290 struct B { 1291 1292 string foo; 1293 string bar; 1294 1295 } 1296 1297 string name; 1298 int number = 1; 1299 B sampleTexts; 1300 string[] notes; 1301 1302 } 1303 1304 auto json = JSONParser(q{ 1305 { 1306 "name": "library", 1307 "sampleTexts": { 1308 "foo": "Dolorem ipsum, quia dolor sit", 1309 "bar": "amet, consectetur, adipisci velit" 1310 }, 1311 "notes": [ 1312 "hello,", 1313 "world!" 1314 ] 1315 } 1316 }); 1317 1318 import std.string : startsWith; 1319 1320 auto a = json.getStruct!A; 1321 1322 assert(a.name == "library"); 1323 assert(a.number == 1); 1324 assert(a.sampleTexts.foo.startsWith("Dolorem ipsum")); 1325 assert(a.sampleTexts.bar.startsWith("amet")); 1326 assert(a.notes == ["hello,", "world!"]); 1327 1328 } 1329 1330 unittest { 1331 1332 struct Test { 1333 1334 int[3] test; 1335 1336 } 1337 1338 auto text = q{ { "test": [1, 2, 3] } }; 1339 auto json = JSONParser(text); 1340 1341 auto obj = json.getStruct!Test; 1342 assert(obj.test == [1, 2, 3]); 1343 1344 } 1345 1346 /// UDA used to exclude struct fields from parsing. 1347 enum JSONExclude; 1348 1349 /// UDA used to rename struct fields during parsing. The value in the attribute represents the name used in the JSON 1350 /// file. 1351 struct JSONName { 1352 1353 string originalName; 1354 1355 } 1356 1357 /// 1358 unittest { 1359 1360 struct Product { 1361 1362 string name; 1363 float price; 1364 1365 @JSONExclude { 1366 float weight; 1367 string weightUnit; 1368 } 1369 1370 } 1371 1372 auto json = JSONParser(q{ 1373 { 1374 "name": "foo", 1375 "price": 123, 1376 "weight": "500g" 1377 } 1378 }); 1379 1380 auto product = json.getStruct!Product((ref Product obj, wstring key) { 1381 1382 import std.uni : isAlpha; 1383 import std.algorithm : countUntil; 1384 1385 if (key == "weight") { 1386 1387 const value = json.getString; 1388 const splitIndex = value.countUntil!isAlpha; 1389 1390 // Extract the unit 1391 obj.weight = value[0..splitIndex].to!float; 1392 obj.weightUnit = value[splitIndex..$].to!string; 1393 } 1394 1395 }); 1396 1397 assert(product.name == "foo"); 1398 assert(product.price == 123); 1399 assert(product.weight == 500f); 1400 assert(product.weightUnit == "g"); 1401 1402 } 1403 1404 /// Thrown if JSON parsing fails. 1405 class JSONException : RCDataException { 1406 1407 mixin basicExceptionCtors; 1408 1409 }