1 module jsonwrap; 2 3 import std.json : JSONValue, JSONType; 4 import std.string : isNumeric, indexOf; 5 import std.typecons : Tuple; 6 import std.conv : to; 7 import std.traits : isIntegral, isSomeString; 8 9 alias JSOB = JsonObjectBuilder; 10 alias JSAB = JsonArrayBuilder; 11 12 // A simple struct. It is returned by .get and .as functions 13 struct SafeValue(T) 14 { 15 @safe @nogc nothrow 16 this(in bool exists, in bool ok, in T value = T.init) 17 { 18 this._exists = exists; 19 this._ok = ok; 20 this.value = value; 21 } 22 23 // Return true if key is found. 24 @property @safe @nogc nothrow exists() inout { return _exists; } 25 26 // Return true if value is read without errors 27 @property @safe @nogc nothrow ok() inout { return _ok; } 28 29 private bool _exists = false; 30 private bool _ok = false; 31 32 T value = T.init; 33 34 alias value this; 35 } 36 37 // It allows you to read deep values inside json. If possibile it converts value to type T. 38 // It returns a SafeValue!T. 39 pure nothrow 40 SafeValue!T as(T)(in JSONValue json, in string path = "", /* lazy */ in T defaultValue = T.init) 41 { 42 // A way to check if to!T is valid 43 pure 44 void tryConv(T, K)(in K value, ref SafeValue!T result) 45 { 46 static if (__traits(compiles,to!T(value))) 47 { 48 result.value = to!T(value); 49 result._ok = true; 50 } 51 else 52 { 53 result.value = defaultValue; 54 result._ok = false; 55 56 } 57 } 58 59 immutable splitted = split_json_path(path); 60 immutable isLast = splitted.remainder.length == 0; 61 JSONValue value; 62 63 // Split the path passed in tokens and take the first JSONValue 64 try 65 { 66 if (json.type() == JSONType.object) value = json[splitted.token]; 67 else if (json.type() == JSONType.array) value = json[to!size_t(splitted.token)]; 68 else value = json; 69 } 70 catch (Exception e) { return SafeValue!T(false, false, defaultValue); } 71 72 immutable type = value.type(); 73 74 // The token is a leaf on json, but it's not a leaf on requested path 75 if (!isLast && type != JSONType.array && type != JSONType.object) 76 return SafeValue!T(false, false, defaultValue); 77 78 SafeValue!T result = SafeValue!T(true, true, defaultValue); 79 80 try 81 { 82 final switch(type) 83 { 84 case JSONType.null_: 85 result._ok = is(T == typeof(null)); 86 break; 87 88 case JSONType.false_: 89 static if (is(T == bool)) result.value = false; 90 else tryConv!T(false, result); 91 break; 92 93 case JSONType.true_: 94 static if (is(T == bool)) result.value = true; 95 else tryConv!T(true, result); 96 break; 97 98 case JSONType.float_: 99 static if (is(T == float) || is(T == double)) result.value = to!T(value.floating()); 100 else tryConv!T(value.floating(), result); 101 break; 102 103 case JSONType.integer: 104 static if (isIntegral!T) result.value = to!T(value.integer()); 105 else tryConv!T(value.integer(), result); 106 break; 107 108 case JSONType.uinteger: 109 static if (isIntegral!T) result.value = to!T(value.uinteger()); 110 else tryConv!T(value.uinteger(), result); 111 break; 112 113 case JSONType..string: 114 static if (isSomeString!T) result.value = to!T(value.str()); 115 else tryConv!T(value.str(), result); 116 break; 117 118 case JSONType.object: 119 if (isLast) 120 { 121 // We are on the last token of path and we have a object. If user asks for a JSONValue it's ok. 122 static if (is(T == JSONValue)) result.value = value.object(); 123 else result._ok = false; 124 } 125 // Recursion: read next part of path 126 else return as!T(value, splitted.remainder, defaultValue); 127 break; 128 129 // Ricorsivo: richiamo per l'elemento indicizzato con il percorso accorciato 130 case JSONType.array: 131 if (isLast) 132 { 133 // We are on the last token of path and we have an array. If user asks for a JSONValue it's ok. 134 static if (is(T == JSONValue)) result.value = value.array(); 135 else result._ok = false; 136 } 137 // Recursion: read next part of path 138 else return as!T(value, splitted.remainder, defaultValue); 139 break; 140 } 141 142 } 143 catch (Exception ce) 144 { 145 // Something goes wrong with conversions. Sorry, we give you back a default value 146 return SafeValue!T(true, false, defaultValue); 147 } 148 149 return result; 150 } 151 152 // Shortcut. You can write as!null instead of as!(typeof(null)) 153 pure nothrow 154 SafeValue!(typeof(null)) as(typeof(null) T)(in JSONValue json, in string path = "") 155 { 156 return as!(typeof(null))(json, path); 157 } 158 159 unittest 160 { 161 immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 162 163 assert(js.as!(typeof(null))("null").ok == true); 164 assert(js.as!(typeof(null))("string").ok == false); 165 assert(js.as!string("/string") == "str"); 166 assert(js.as!string("/obj/int") == "1"); 167 assert(js.as!int("/obj/arr/0") == 1); 168 assert(js.as!int("/obj/arr/1") == 2); 169 assert(js.as!float("/obj/float") == 3.0f); 170 assert(js.as!int("/obj/int/blah").exists == false); 171 assert(js.as!string("bau").exists == false); 172 assert(js.as!int("/string").exists == true); 173 assert(js.as!int("/string").ok == false); 174 } 175 176 // Works like as!T but it doesn't convert between types. 177 pure nothrow 178 SafeValue!T get(T)(in JSONValue json, in string path = "", in T defaultValue = T.init) 179 { 180 alias Ret = SafeValue!T; 181 182 immutable splitted = split_json_path(path); 183 immutable isLast = splitted.remainder.length == 0; 184 JSONValue value; 185 186 // Split the path passed in tokens and take the first JSONValue 187 try 188 { 189 if (json.type() == JSONType.object) value = json[splitted.token]; 190 else if (json.type() == JSONType.array) value = json[to!size_t(splitted.token)]; 191 else value = json; 192 } 193 catch (Exception e) 194 { 195 return Ret(false, false, defaultValue); 196 } 197 198 immutable type = value.type(); 199 200 // The token is a leaf on json, but it's not a leaf on requested path 201 if (!isLast && type != JSONType.array && type != JSONType.object) 202 return Ret(false, false, defaultValue); 203 204 try 205 { 206 final switch(type) 207 { 208 case JSONType.null_: static if (is(T == typeof(null))) return Ret(true, true, null); else break; 209 case JSONType.false_: static if (is(T == bool)) return Ret(true, true, false); else break; 210 case JSONType.true_: static if (is(T == bool)) return Ret(true, true, true); else break; 211 case JSONType.float_: static if (is(T == float) || is(T == double)) return Ret(true, true, value.floating()); else break; 212 case JSONType.integer: static if (isIntegral!T) return Ret(true, true, to!T(value.integer())); else break; 213 case JSONType.uinteger: static if (isIntegral!T) return Ret(true, true, to!T(value.uinteger())); else break; 214 case JSONType..string: static if (isSomeString!T) return Ret(true, true, value.str()); else break; 215 216 case JSONType.object: 217 if (isLast) { 218 // See also: as!T 219 static if (is(T == JSONValue)) 220 return Ret(true, true, JSONValue(value.object)); 221 else break; 222 } 223 else return get!T(value, splitted.remainder, defaultValue); 224 225 case JSONType.array: 226 if (isLast) { 227 // See also: as!T 228 static if (is(T == JSONValue)) 229 return Ret(true, true, JSONValue(value.array)); 230 else break; 231 } 232 else return get!T(value, splitted.remainder, defaultValue); 233 } 234 } 235 catch (Exception e) 236 { 237 return Ret(true, false, defaultValue); 238 } 239 240 // Wrong conversion requested. 241 return Ret(true, false, defaultValue); 242 } 243 244 // Shortcut. You can write get!null instead of get!(typeof(null)) 245 pure nothrow 246 SafeValue!(typeof(null)) get(typeof(null) T)(in JSONValue json, in string path = "") 247 { 248 return get!(typeof(null))(json, path); 249 } 250 251 unittest 252 { 253 immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 254 255 assert(js.get!(typeof(null))("null").ok == true); 256 assert(js.get!(typeof(null))("string").ok == false); 257 assert(js.get!string("/string") == "str"); 258 259 assert(js.get!string("/obj/int").ok == false); 260 assert(js.get!string("/obj/int") == string.init); 261 262 assert(js.get!int("/obj/arr/0").ok == false); 263 assert(js.get!int("/obj/arr/0") == int.init); 264 265 assert(js.get!int("/obj/arr/1") == 2); 266 assert(js.get!float("/obj/float") == 3.0f); 267 assert(js.get!int("/obj/int/blah").exists == false); 268 assert(js.get!string("bau").exists == false); 269 assert(js.get!int("/string").exists == true); 270 assert(js.get!int("/string").ok == false); 271 } 272 273 unittest 274 { 275 immutable js = JSOB("notnull", 0, "null", null); 276 277 assert(js.as!null("/null").ok == true); 278 assert(js.as!null("/notnull").ok == false); 279 280 assert(js.get!null("/null").ok == true); 281 assert(js.get!null("/notnull").ok == false); 282 } 283 284 // Works like get but return T instead of SafeValue!T and throw an exception if something goes wrong (can't convert value or can't find key) 285 pure 286 T read(T)(in JSONValue json, in string path = "") 287 { 288 auto ret = get!T(json, path); 289 290 if (!ret.ok || !ret.exists) 291 throw new Exception("Can't read " ~ path ~ " from json"); 292 293 return ret.value; 294 } 295 296 unittest 297 { 298 import std.exception: assertThrown; 299 immutable js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 300 301 assert(js.read!string("string") == "str"); 302 assert(js.read!int("/obj/int") == 1); 303 assertThrown(js.read!int("string")); 304 assertThrown(js.read!int("other")); 305 } 306 307 308 // Write a value. It creates missing objects and array (also missing elements) 309 pure 310 ref JSONValue put(T)(ref JSONValue json, in string path, in T value) 311 { 312 // Take a token from path 313 immutable splitted = split_json_path(path); 314 immutable isLast = splitted.remainder.length == 0; 315 316 enum nullValue = JSONValue(null); 317 318 // If token is a number, we are trying to write an array. 319 if (isNumeric(splitted.token)) 320 { 321 immutable idx = to!size_t(splitted.token); 322 323 // Are we reading an existing element from an existing array? 324 if (json.type == JSONType.array && json.array.length > idx) 325 { 326 if (!isLast) put!T(json.array[idx], splitted.remainder, value); 327 else json.array[idx] = value; 328 } 329 else 330 { 331 if (json.type != JSONType.array) 332 json = JSONValue[].init; 333 334 json.array.length = idx+1; 335 336 if (!isLast) put!T(json.array[idx], splitted.remainder, value); 337 else json.array[idx] = value; 338 } 339 } 340 // If token is *NOT* a number, we are trying to write an object. 341 else 342 { 343 immutable idx = splitted.token; 344 345 // Are we reading an existing object? 346 if (json.type == JSONType.object) 347 { 348 if (!isLast) 349 { 350 if (idx !in json.object) 351 json.object[idx] = nullValue; 352 353 put!T(json.object[idx], splitted.remainder, value); 354 } 355 else json.object[idx] = value; 356 } 357 else 358 { 359 json = string[string].init; 360 361 if (!isLast) 362 { 363 json.object[idx] = nullValue; 364 put!T(json.object[idx], splitted.remainder, value); 365 } 366 else json.object[idx] = value; 367 } 368 } 369 370 return json; 371 } 372 373 unittest 374 { 375 auto js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 376 377 js.put("/string", "hello"); 378 js.put("/null/not", 10); 379 js.put("/obj/arr/3", JSOB); 380 js.put("hello", "world"); 381 382 assert(js.get!string("/string") == "hello"); 383 assert(js.get!int("/null/not") == 10); 384 assert(js.get!null("/obj/arr/2").ok); 385 assert(js.get!JSONValue("/obj/arr/3") == JSOB); 386 assert(js.get!JSONValue("/obj/arr/3").ok == true); 387 assert(js.get!string("hello") == "world"); 388 } 389 390 // Remove a field (if it exists). It returns the object itself 391 pure 392 ref JSONValue remove(ref JSONValue json, in string path) 393 { 394 immutable splitted = split_json_path(path); 395 immutable isLast = splitted.remainder.length == 0; 396 397 // See above 398 if (isNumeric(splitted.token)) 399 { 400 immutable idx = to!size_t(splitted.token); 401 402 if (json.type == JSONType.array && json.array.length > idx) 403 { 404 if (isLast) json.array = json.array[0..idx] ~ json.array[idx+1 .. $]; 405 else json.array[idx].remove(splitted.remainder); 406 } 407 408 } 409 else 410 { 411 immutable idx = splitted.token; 412 413 if (json.type == JSONType.object && idx in json.object) 414 { 415 if (isLast) json.object.remove(idx); 416 else json.object[idx].remove(splitted.remainder); 417 } 418 } 419 420 421 return json; 422 } 423 424 // Check if a field exists or not 425 pure 426 bool exists(in JSONValue json, in string path) 427 { 428 immutable splitted = split_json_path(path); 429 immutable isLast = splitted.remainder.length == 0; 430 431 // See above 432 if (isNumeric(splitted.token)) 433 { 434 immutable idx = to!size_t(splitted.token); 435 436 if (json.type == JSONType.array && json.array.length > idx) 437 { 438 if (isLast) return true; 439 else return json.array[idx].exists(splitted.remainder); 440 } 441 442 } 443 else 444 { 445 immutable idx = splitted.token; 446 447 if (json.type == JSONType.object && idx in json.object) 448 { 449 if (isLast) return true; 450 else return json.object[idx].exists(splitted.remainder); 451 } 452 } 453 454 return false; 455 } 456 457 unittest 458 { 459 auto js = JSOB("string", "str", "null", null, "obj", JSOB("int", 1, "float", 3.0f, "arr", JSAB("1", 2))); 460 461 js.put("/string", "hello"); 462 js.put("/null/not", 10); 463 js.put("/obj/arr/3", JSOB); 464 js.put("hello", "world"); 465 466 js.remove("/obj/arr/2"); 467 js.remove("string"); 468 469 assert(js.exists("/string") == false); 470 assert(js.exists("/obj/arr/3") == false); 471 assert(js.exists("/obj/arr/2") == true); 472 assert(js.get!JSONValue("/obj/arr/2") == JSOB); 473 } 474 475 476 private alias SplitterResult = Tuple!(string, "token", string, "remainder"); 477 478 // Used to split path like /hello/world in tokens 479 pure nothrow @safe @nogc 480 private SplitterResult split_json_path(in string path) 481 { 482 immutable idx = path.indexOf('/'); 483 484 switch (idx) 485 { 486 case 0: return split_json_path(path[1..$]); 487 case -1: return SplitterResult(path, string.init); 488 default: return SplitterResult(path[0..idx], path[idx+1..$]); 489 } 490 491 assert(0); 492 } 493 494 // You can build a json object with JsonObjectBuilder("key", 32, "another_key", "hello", "subobject", JsonObjectBuilder(...)); 495 pure 496 JSONValue JsonObjectBuilder(T...)(T vals) 497 { 498 void appendJsonVals(T...)(ref JSONValue value, T vals) 499 { 500 // Appends nothing, recursion ends 501 static if (vals.length == 0) return; 502 503 // We're working with a tuple (key, value, key, value, ...) so args%2==0 is key and args%2==1 is value 504 else static if (vals.length % 2 == 0) 505 { 506 // Key should be a string! 507 static if (!isSomeString!(typeof(vals[0]))) 508 throw new Exception("Wrong param type. Key not valid."); 509 510 else value[vals[0]] = vals[1]; 511 512 // Recursion call 513 static if (vals.length > 2) 514 appendJsonVals(value, vals[2..$]); 515 516 } else throw new Exception("Wrong params. Should be: JsonObjectBuilder(string key1, T1 val1, string key2, T2 val2, ...)"); 517 } 518 519 JSONValue value = string[string].init; 520 521 static if (vals.length > 0) 522 appendJsonVals(value, vals); 523 524 return value; 525 } 526 527 // You can build a json array with JsonArrayBuilder("first", 32, "another_element", 2, 23.4, JsonObjectBuilder(...)); 528 pure 529 JSONValue JsonArrayBuilder(T...)(T vals) 530 { 531 JSONValue value = JSONValue[].init; 532 value.array.length = vals.length; 533 534 foreach(idx, v; vals) 535 value[idx] = v; 536 537 return value; 538 } 539 540 unittest 541 { 542 { 543 enum js = JSOB("array", JSAB(1,2,"blah"), "subobj", JSOB("int", 1, "string", "str", "array", [1,2,3])); 544 assert(js.get!int("/array/1") == 2); 545 assert(js.get!int("/subobj/int") == 1); 546 assert(js.get!string("/subobj/string") == "str"); 547 assert(js.as!string("/subobj/array/2") == "3"); 548 assert(js.exists("/subobj/string") == true); 549 assert(js.exists("/subobj/other") == false); 550 551 // /array/1 it's an integer 552 { 553 // Can't get a string 554 { 555 immutable val = js.get!string("/array/1", "default"); 556 assert(val.exists == true); 557 assert(val.ok == false); 558 assert(val == "default"); 559 } 560 561 // Can read as string 562 { 563 immutable val = js.as!string("/array/1", "default"); 564 assert(val.exists == true); 565 assert(val.ok == true); 566 assert(val == "2"); 567 } 568 } 569 570 571 // This value doesn't exist 572 { 573 immutable val = js.as!string("/subobj/other", "default"); 574 assert(val.exists == false); 575 assert(val.ok == false); 576 assert(val == "default"); 577 } 578 579 580 // Value exists but can't convert to int 581 { 582 immutable val = js.as!int("/array/2", 15); 583 assert(val.exists == true); 584 assert(val.ok == false); 585 assert(val == 15); 586 } 587 588 // Can't edit an enum, of course 589 assert(__traits(compiles, js.remove("/subobj/string")) == false); 590 591 // But I can edit a copy 592 JSONValue cp = js; 593 assert(cp == js); 594 assert(cp.toString == js.toString); 595 596 cp.remove("/subobj/string"); 597 assert(cp.exists("/subobj/string") == false); 598 assert(cp.exists("/subobj/int") == true); 599 600 } 601 } 602 603 604 unittest 605 { 606 import std.json : parseJSON; 607 608 // Standard way 609 JSONValue json = parseJSON(`{"user" : "foo", "address" : {"city" : "venice", "country" : "italy"}, "tags" : ["hello" , 3 , {"key" : "value"}]}`); 610 611 { 612 string user = json.get!string("user"); // Read a string from json 613 assert(user == "foo"); 614 } 615 616 { 617 // Read a string, user is a SafeValue!string 618 auto user = json.get!string("user"); 619 assert(user.ok == true); 620 assert(user.exists == true); 621 622 // This field doesn't exists on json 623 // I can set a default value 624 auto notfound = json.get!string("blah", "my default value"); 625 assert(notfound.ok == false); 626 assert(notfound.exists == false); 627 assert(notfound == "my default value"); 628 629 // This field exists but it's not an int, it's a string 630 auto wrong = json.get!int("user"); 631 assert(wrong.ok == false); 632 assert(wrong.exists == true); 633 assert(wrong == int.init); 634 } 635 636 { 637 // I can read deep fields 638 assert(json.get!string("/address/city") == "venice"); 639 640 // also inside an array 641 assert(json.get!string("/tags/2/key") == "value"); 642 } 643 644 { 645 // Using as!T you can convert field 646 assert(json.as!string("/tags/1") == "3"); // On json "/tags/1" is an int. 647 } 648 649 { 650 // You can check if a field exists or not 651 assert(json.exists("/address/country") == true); 652 653 // You can remove it 654 json.remove("/address/country"); 655 656 // It doesn't exists anymore 657 assert(json.exists("/address/country") == false); 658 } 659 660 { 661 // You can write using put. 662 json.put("/address/country", "italy"); // Restore deleted field 663 json.put("/this/is/a/deep/value", 100); // It create the whole tree 664 json.put("/this/is/an/array/5", "hello"); // Ditto 665 666 assert(json.get!int("/this/is/a/deep/value") == 100); 667 assert(json.get!string("/this/is/an/array/5") == "hello"); // elements 0,1,2,3,4 are nulled 668 } 669 670 { 671 // A fast way to build object CTFE compatible. 672 // JSOB is an alias for JsonObjectBuilder and JSAB for JsonArrayBuilder 673 JSONValue jv = JSOB 674 ( 675 "key", "value", 676 "obj", JSOB("subkey", 3), 677 "array", [1,2,3], 678 "mixed_array", JSAB(1, "hello", 3.0f) 679 ); 680 681 assert(jv.toString == `{"array":[1,2,3],"key":"value","mixed_array":[1,"hello",3],"obj":{"subkey":3}}`); 682 } 683 684 { 685 JSONValue jv = JSOB 686 ( 687 "key", "value", 688 "obj", JSOB("subkey", 3), 689 "array", [1,2,3], 690 "mixed_array", JSAB(1, "hello", 3.0f) 691 ); 692 693 foreach(size_t idx, o; jv.get!JSONValue("/array")) 694 { 695 assert(o.get!int("/") == idx+1); 696 assert(o.as!float("") == idx+1); 697 assert(o.read!int("/")== idx+1); 698 assert(o.get!int == idx+1); 699 assert(o.as!float == idx+1); 700 assert(o.read!int == idx+1); 701 } 702 } 703 }