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 }