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