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 }