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 }