1 /++
2     Simple JSON wrappers around Phobos' [std.json] to make keeping JSON storages easier.
3     This is not a replacement for [std.json]; it merely extends it.
4 
5     Example:
6     ---
7     JSONStorage json;
8     assert(json.storage.type == JSONType.null_);
9 
10     json.load("somefile.json");
11     assert(json.storage.type == JSONType.object);
12 
13     json.serialiseInto!(JSONStorage.KeyOrderStrategy.inGivenOrder)
14         (stdout.lockingTextWriter, [ "foo", "bar", "baz" ]);
15 
16     // Printed to screen, regardless how `.toPrettyString` would have ordered it:
17     /*
18         {
19             "foo": {
20                 1,
21                 2,
22             },
23             "bar": {
24                 3,
25                 4,
26             },
27             "baz": {
28                 5,
29                 6,
30             }
31         }
32     */
33 
34     // Prints keys in sorted order.
35     json.serialiseInto!(JSONStorage.KeyOrderStrategy.sorted)(stdout.lockingTextWriter)
36 
37     // Use a [std.array.Appender|Appender] to serialise into a string.
38 
39     // Adding and removing values still needs the same dance as with std.json.
40     // Room for future improvement.
41     json.storage["qux"] = null;
42     json.storage["qux"].array = null;
43     json.storage["qux"].array ~= 7;
44     json.storage["qux"].array ~= 8;
45 
46     json.save("somefile.json");
47     ---
48 
49     Copyright: [JR](https://github.com/zorael)
50     License: [Boost Software License 1.0](https://www.boost.org/users/license.html)
51 
52     Authors:
53         [JR](https://github.com/zorael)
54  +/
55 module lu.json;
56 
57 private:
58 
59 import std.json : JSONValue;
60 import std.traits : isMutable;
61 
62 public:
63 
64 
65 // JSONStorage
66 /++
67     A wrapped [std.json.JSONValue|JSONValue] with helper functions.
68 
69     Example:
70     ---
71     JSONStorage s;
72 
73     s.reset();  // not always necessary
74 
75     s.storage["foo"] = null;  // JSONValue quirk
76     s.storage["foo"]["abc"] = JSONValue(42);
77     s.storage["foo"]["def"] = JSONValue(3.14f);
78     s.storage["foo"]["ghi"] = JSONValue([ "bar", "baz", "qux" ]);
79     s.storage["bar"] = JSONValue("asdf");
80 
81     assert(s.storage.length == 2);
82     ---
83  +/
84 struct JSONStorage
85 {
86 private:
87     import std.json : JSONValue, parseJSON;
88 
89 public:
90     /++
91         The underlying [std.json.JSONValue|JSONValue] storage of this [JSONStorage].
92      +/
93     JSONValue storage;
94 
95     alias storage this;
96 
97     /++
98         Strategy in which to sort object-type JSON keys when we format/serialise
99         the stored [storage] to string.
100      +/
101     enum KeyOrderStrategy
102     {
103         /++
104             Order is as [std.json.JSONValue.toPrettyString|JSONValue.toPrettyString]
105             formats it.
106          +/
107         passthrough,
108 
109         /++
110             Sorted by key.
111          +/
112         sorted,
113 
114         /++
115             Reversely sorted by key.
116          +/
117         reverse,
118 
119         /++
120             Keys are listed in the order given in a passed `string[]` array.
121 
122             Actual keys not present in the array are not included in the output,
123             and keys not existing yet present in the array are added as empty.
124          +/
125         inGivenOrder,
126     }
127 
128     // reset
129     /++
130         Initialises and clears the [std.json.JSONValue|JSONValue], preparing
131         it for object storage.
132      +/
133     void reset() @safe pure nothrow @nogc
134     {
135         storage.object = null;
136     }
137 
138     // load
139     /++
140         Loads JSON from disk.
141 
142         In the case where the file doesn't exist or is otherwise invalid, then
143         [std.json.JSONValue|JSONValue] is initialised to null (by way of
144         [JSONStorage.reset]).
145 
146         Params:
147             filename = Filename of file to read from.
148 
149         Throws:
150             Whatever [std.file.readText|readText] and/or
151             [std.json.parseJSON|parseJSON] throws.
152 
153             [lu.common.FileTypeMismatchException] if the filename exists
154             but is not a file.
155      +/
156     void load(const string filename) @safe
157     in (filename.length, "Tried to load an empty filename into a JSON storage")
158     {
159         import lu.common : FileTypeMismatchException;
160         import std.file : exists, getAttributes, isFile, readText;
161         import std.path : baseName;
162 
163         if (!filename.exists)
164         {
165             return reset();
166         }
167         else if (!filename.isFile)
168         {
169             reset();
170             throw new FileTypeMismatchException("File exists but is not a file.",
171                 filename.baseName, cast(ushort)getAttributes(filename));
172         }
173 
174         immutable fileContents = readText(filename);
175         storage = parseJSON(fileContents.length ? fileContents : "{}");
176     }
177 
178 
179     // save
180     /++
181         Saves the JSON storage to disk. Formatting is done as specified by the
182         passed [KeyOrderStrategy] argument.
183 
184         Merely leverages [serialiseInto] and [std.stdio.writeln|writeln].
185 
186         Params:
187             strategy = Key order strategy in which to sort object-type JSON keys.
188             filename = Filename of the file to save to.
189             givenOrder = The order in which object-type keys should be listed in
190                 the output file. Non-existent keys are represented as empty. Not
191                 specified keys are omitted.
192      +/
193     void save(KeyOrderStrategy strategy = KeyOrderStrategy.passthrough)
194         (const string filename,
195         const string[] givenOrder = string[].init) @safe
196     in (filename.length, "Tried to save a JSON storage to an empty filename")
197     {
198         import std.array : Appender;
199         import std.json : JSONType;
200         import std.stdio : File, writeln;
201 
202         Appender!(char[]) sink;
203         sink.reserve(1024);  // guesstimate
204 
205         if (storage.type == JSONType.object)
206         {
207             static if (strategy == KeyOrderStrategy.inGivenOrder)
208             {
209                 serialiseInto!strategy(sink, givenOrder);
210             }
211             else
212             {
213                 serialiseInto!strategy(sink);
214             }
215         }
216         else
217         {
218             serialiseInto!(KeyOrderStrategy.passthrough)(sink);
219         }
220 
221         File(filename, "w").writeln(sink.data);
222     }
223 
224 
225     // serialiseInto
226     /++
227         Formats an object-type JSON storage into an output range sink.
228 
229         Top-level keys are sorted as per the passed [KeyOrderStrategy]. This
230         overload is specialised for [KeyOrderStrategy.inGivenOrder].
231 
232         Params:
233             strategy = Order strategy in which to sort top-level keys.
234             sink = Output sink to fill with formatted output.
235             givenOrder = The order in which object-type keys should be listed in
236                 the output file. Non-existent keys are represented as empty.
237                 Not specified keys are omitted.
238      +/
239     void serialiseInto(KeyOrderStrategy strategy : KeyOrderStrategy.inGivenOrder, Sink)
240         (auto ref Sink sink, const string[] givenOrder) @safe
241     in (givenOrder.length, "Tried to serialise a JSON storage in order given without a given order")
242     {
243         import std.range.primitives : isOutputRange;
244 
245         static if (!isOutputRange!(Sink, char[]))
246         {
247             enum message = "`serialiseInto` sink must be an output range accepting `char[]`";
248             static assert(0, message);
249         }
250 
251         if (storage.isNull)
252         {
253             sink.put("{}");
254             return;
255         }
256 
257         sink.put("{\n");
258 
259         foreach (immutable i, immutable key; givenOrder)
260         {
261             import std.format : formattedWrite;
262 
263             sink.formattedWrite("    \"%s\": ", key);
264 
265             if (const entry = key in storage)
266             {
267                 import lu.string : indentInto;
268                 entry.toPrettyString.indentInto(sink, 1, 1);
269             }
270             else
271             {
272                 sink.put("{}");
273             }
274 
275             sink.put((i+1 < givenOrder.length) ? ",\n" : "\n");
276         }
277 
278         sink.put("}");
279     }
280 
281 
282     // serialiseInto
283     /++
284         Formats an object-type JSON storage into an output range sink.
285 
286         Top-level keys are sorted as per the passed [KeyOrderStrategy]. This
287         overload is specialised for strategies other than
288         [KeyOrderStrategy.inGivenOrder], and as such takes one parameter fewer.
289 
290         Params:
291             strategy = Order strategy in which to sort top-level keys.
292             sink = Output sink to fill with formatted output.
293      +/
294     void serialiseInto(KeyOrderStrategy strategy = KeyOrderStrategy.passthrough, Sink)
295         (auto ref Sink sink) @safe
296     if (strategy != KeyOrderStrategy.inGivenOrder)
297     {
298         import std.range.primitives : isOutputRange;
299 
300         static if (!isOutputRange!(Sink, char[]))
301         {
302             enum message = "`serialiseInto` sink must be an output range accepting `char[]`";
303             static assert(0, message);
304         }
305 
306         if (storage.isNull)
307         {
308             sink.put("{}");
309             return;
310         }
311 
312         static if (strategy == KeyOrderStrategy.passthrough)
313         {
314             // Just pass through and save .toPrettyString; keep original behaviour.
315             sink.put(storage.toPrettyString);
316         }
317         else static if ((strategy == KeyOrderStrategy.sorted) ||
318             (strategy == KeyOrderStrategy.reverse))
319         {
320             import std.array : array;
321             import std.format : formattedWrite;
322             import std.range : enumerate;
323             import std.algorithm.sorting : sort;
324 
325             auto rawRange = storage
326                 .objectNoRef
327                 .byKey
328                 .array
329                 .sort;
330 
331             static if (strategy == KeyOrderStrategy.reverse)
332             {
333                 import std.range : retro;
334                 auto range = rawRange.retro;
335             }
336             else static if (strategy == KeyOrderStrategy.sorted)
337             {
338                 // Already sorted
339                 alias range = rawRange;
340             }
341             else
342             {
343                 static assert(0, "Logic error; unexpected `KeyOrderStrategy` " ~
344                     "passed to `serialiseInto`");
345             }
346 
347             sink.put("{\n");
348 
349             foreach(immutable i, immutable key; range.enumerate)
350             {
351                 import lu.string : indentInto;
352                 sink.formattedWrite("    \"%s\": ", key);
353                 storage[key].toPrettyString.indentInto(sink, 1, 1);
354                 sink.put((i+1 < range.length) ? ",\n" : "\n");
355             }
356 
357             sink.put("}");
358         }
359         else
360         {
361             static assert(0, "Logic error; invalid `KeyOrderStrategy` " ~
362                 "passed to `serialiseInto`");
363         }
364     }
365 
366     ///
367     @system unittest
368     {
369         import std.array : Appender;
370         import std.json;
371 
372         JSONStorage this_;
373         Appender!(char[]) sink;
374 
375         // Original JSON
376         this_.storage = parseJSON(
377 `{
378 "#abc":
379 {
380 "hirrsteff" : "o",
381 "foobar" : "v"
382 },
383 "#def":
384 {
385 "harrsteff": "v",
386 "flerpeloso" : "o"
387 },
388 "#zzz":
389 {
390 "asdf" : "v"
391 }
392 }`);
393 
394         // KeyOrderStrategy.passthrough
395         this_.serialiseInto!(KeyOrderStrategy.passthrough)(sink);
396         assert((sink.data ==
397 `{
398     "#abc": {
399         "foobar": "v",
400         "hirrsteff": "o"
401     },
402     "#def": {
403         "flerpeloso": "o",
404         "harrsteff": "v"
405     },
406     "#zzz": {
407         "asdf": "v"
408     }
409 }`), '\n' ~ sink.data);
410         sink.clear();
411 
412         // KeyOrderStrategy.sorted
413         this_.serialiseInto!(KeyOrderStrategy.sorted)(sink);
414         assert((sink.data ==
415 `{
416     "#abc": {
417         "foobar": "v",
418         "hirrsteff": "o"
419     },
420     "#def": {
421         "flerpeloso": "o",
422         "harrsteff": "v"
423     },
424     "#zzz": {
425         "asdf": "v"
426     }
427 }`), '\n' ~ sink.data);
428         sink.clear();
429 
430         // KeyOrderStrategy.reverse
431         this_.serialiseInto!(KeyOrderStrategy.reverse)(sink);
432         assert((sink.data ==
433 `{
434     "#zzz": {
435         "asdf": "v"
436     },
437     "#def": {
438         "flerpeloso": "o",
439         "harrsteff": "v"
440     },
441     "#abc": {
442         "foobar": "v",
443         "hirrsteff": "o"
444     }
445 }`), '\n' ~ sink.data);
446         sink.clear();
447 
448         // KeyOrderStrategy.inGivenOrder
449         this_.serialiseInto!(KeyOrderStrategy.inGivenOrder)(sink, [ "#def", "#abc", "#foo", "#fighters" ]);
450         assert((sink.data ==
451 `{
452     "#def": {
453         "flerpeloso": "o",
454         "harrsteff": "v"
455     },
456     "#abc": {
457         "foobar": "v",
458         "hirrsteff": "o"
459     },
460     "#foo": {},
461     "#fighters": {}
462 }`), '\n' ~ sink.data);
463         sink.clear();
464 
465         // Empty JSONValue
466         JSONStorage this2;
467         this2.serialiseInto(sink);
468         assert((sink.data ==
469 `{}`), '\n' ~ sink.data);
470     }
471 }
472 
473 ///
474 unittest
475 {
476     import std.conv : text;
477     import std.json : JSONValue;
478 
479     JSONStorage s;
480     s.reset();
481 
482     s.storage["key"] = null;
483     s.storage["key"]["subkey1"] = "abc";
484     s.storage["key"]["subkey2"] = "def";
485     s.storage["key"]["subkey3"] = "ghi";
486     assert((s.storage["key"].object.length == 3), s.storage["key"].object.length.text);
487 
488     s.storage["foo"] = null;
489     s.storage["foo"]["arr"] = JSONValue([ "blah "]);
490     s.storage["foo"]["arr"].array ~= JSONValue("bluh");
491     assert((s.storage["foo"]["arr"].array.length == 2), s.storage["foo"]["arr"].array.length.text);
492 }
493 
494 
495 // populateFromJSON
496 /++
497     Recursively populates a passed associative or dynamic array with the
498     contents of a [std.json.JSONValue|JSONValue].
499 
500     This is used where we want to store information on disk but keep it in
501     memory without the overhead of dealing with [std.json.JSONValue|JSONValue]s.
502 
503     Note: This only works with [std.json.JSONValue|JSONValue]s that conform to
504     arrays and associative arrays, not such that mix element/value types.
505 
506     Params:
507         target = Reference to target array or associative array to write to.
508         json = Source [std.json.JSONValue|JSONValue] to sync the contents with.
509         lowercaseKeys = Whether or not to save string keys in lowercase.
510         lowercaseValues = Whether or not to save final string values in lowercase.
511 
512     Throws:
513         [object.Exception|Exception] if the passed [std.json.JSONValue|JSONValue]
514         had unexpected types.
515  +/
516 void populateFromJSON(T)
517     (ref T target,
518     const JSONValue json,
519     const bool lowercaseKeys = false,
520     const bool lowercaseValues = false) @safe
521 if (isMutable!T)
522 {
523     import std.traits : ValueType, isAssociativeArray, isArray, isDynamicArray, isSomeString;
524     import std.range : ElementEncodingType;
525 
526     static if (isAssociativeArray!T || (isArray!T && !isSomeString!T))
527     {
528         static if (isAssociativeArray!T)
529         {
530             const aggregate = json.objectNoRef;
531             alias Value = ValueType!T;
532         }
533         else static if (isArray!T)
534         {
535             const aggregate = json.arrayNoRef;
536             alias Value = ElementEncodingType!T;
537 
538             static if (isDynamicArray!T)
539             {
540                 target.reserve(aggregate.length);
541             }
542         }
543         else
544         {
545             static assert(0, "`populateFromJSON` was passed an unsupported type `" ~ T.stringof ~ "`");
546         }
547 
548         foreach (ikey, const valJSON; aggregate)
549         {
550             static if (isAssociativeArray!T)
551             {
552                 static if (isSomeString!Value)
553                 {
554                     if (lowercaseKeys)
555                     {
556                         import std.uni : toLower;
557                         ikey = ikey.toLower;
558                     }
559                 }
560 
561                 target[ikey] = Value.init;
562             }
563             else static if (isDynamicArray!T)
564             {
565                 if (ikey >= target.length) target ~= Value.init;
566             }
567 
568             populateFromJSON(target[ikey], valJSON);
569         }
570 
571         /*static if (isAssociativeArray!T)
572         {
573             // This would make it @system.
574             target.rehash();
575         }*/
576     }
577     else
578     {
579         import std.conv : to;
580         import std.json : JSONType;
581 
582         with (JSONType)
583         final switch (json.type)
584         {
585         case string:
586             target = json.str.to!T;
587 
588             static if (isSomeString!T)
589             {
590                 if (lowercaseValues)
591                 {
592                     import std.uni : toLower;
593                     target = target.toLower;
594                 }
595             }
596             break;
597 
598         case integer:
599             // .integer returns long, keep .to for int compatibility
600             target = json.integer.to!T;
601             break;
602 
603         case uinteger:
604             // as above
605             target = json.uinteger.to!T;
606             break;
607 
608         case float_:
609             target = json.floating.to!T;
610             break;
611 
612         case true_:
613         case false_:
614             target = json.boolean.to!T;
615             break;
616 
617         case null_:
618             // Silently do nothing
619             break;
620 
621         case object:
622         case array:
623             import std.format : format;
624             throw new Exception("Type mismatch when populating a `%s` with a `%s`"
625                 .format(T.stringof, json.type));
626         }
627     }
628 }
629 
630 ///
631 unittest
632 {
633     import std.json : JSONType, JSONValue;
634 
635     {
636         long[string] aa =
637         [
638             "abc" : 123,
639             "def" : 456,
640             "ghi" : 789,
641         ];
642 
643         JSONValue j = JSONValue(aa);
644         typeof(aa) fromJSON;
645 
646         foreach (immutable key, const value; j.objectNoRef)
647         {
648             fromJSON[key] = value.integer;
649         }
650 
651         assert(aa == fromJSON);  // not is
652 
653         auto aaCopy = aa.dup;
654 
655         aa["jlk"] = 12;
656         assert(aa != fromJSON);
657 
658         aa = typeof(aa).init;
659         populateFromJSON(aa, j);
660         assert(aa == aaCopy);
661     }
662     {
663         auto aa =
664         [
665             "abc" : true,
666             "def" : false,
667             "ghi" : true,
668         ];
669 
670         JSONValue j = JSONValue(aa);
671         typeof(aa) fromJSON;
672 
673         foreach (immutable key, const value; j.objectNoRef)
674         {
675             if (value.type == JSONType.true_) fromJSON[key] = true;
676             else if (value.type == JSONType.false_) fromJSON[key] = false;
677             else
678             {
679                 assert(0);
680             }
681         }
682 
683         assert(aa == fromJSON);  // not is
684 
685         auto aaCopy = aa.dup;
686 
687         aa["jkl"] = false;
688         assert(aa != fromJSON);
689 
690         aa = typeof(aa).init;
691         populateFromJSON(aa, j);
692         assert(aa == aaCopy);
693     }
694     {
695         auto arr = [ "abc", "def", "ghi", "jkl" ];
696 
697         JSONValue j = JSONValue(arr);
698         typeof(arr) fromJSON;
699 
700         foreach (const value; j.arrayNoRef)
701         {
702             fromJSON ~= value.str;
703         }
704 
705         assert(arr == fromJSON);  // not is
706 
707         auto arrCopy = arr.dup;
708 
709         arr[0] = "no";
710         assert(arr != arrCopy);
711 
712         arr = [];
713         populateFromJSON(arr, j);
714         assert(arr == arrCopy);
715     }
716     {
717         auto aa =
718         [
719             "abc" : [ "def", "ghi", "jkl" ],
720             "def" : [ "MNO", "PQR", "STU" ],
721         ];
722 
723         JSONValue j = JSONValue(aa);
724         typeof(aa)fromJSON;
725 
726         foreach (immutable key, const arrJSON; j.objectNoRef)
727         {
728             foreach (const entry; arrJSON.arrayNoRef)
729             {
730                 fromJSON[key] ~= entry.str;
731             }
732         }
733 
734         assert(aa == fromJSON);  // not is
735 
736         auto aaCopy = aa.dup;
737         aaCopy["abc"] = aa["abc"].dup;
738 
739         aa["abc"][0] = "no";
740         aa["ghi"] ~= "VWXYZ";
741         assert(aa != fromJSON);
742 
743         aa = typeof(aa).init;
744         populateFromJSON(aa, j);
745         assert(aa == aaCopy);
746     }
747     {
748         int[3] arr = [ 1, 2, 3 ];
749 
750         JSONValue j = JSONValue(arr);
751 
752         int[3] arr2;
753         arr2.populateFromJSON(j);
754         assert(arr2 == arr);
755     }
756 }