1 /// serialize and deserialize between JSONValues and other D types 2 module jsonizer; 3 4 import std.json; 5 import std.file; 6 import std.conv; 7 import std.range; 8 import std.traits; 9 import std.string; 10 import std.algorithm; 11 import std.exception; 12 import std.typetuple; 13 import std.typecons : staticIota; 14 15 public import internal.io; 16 public import internal.tojson; 17 public import internal.extract; 18 public import internal.attribute; 19 20 21 string jsonizeKey(alias obj, string memberName)() { 22 foreach(attr ; __traits(getAttributes, mixin("obj." ~ memberName))) { 23 static if (is(attr == jsonize)) { // @jsonize someMember; 24 return memberName; // use member name as-is 25 } 26 else static if (is(typeof(attr) == jsonize)) { // @jsonize("someKey") someMember; 27 return attr.key; 28 } 29 } 30 return null; 31 } 32 33 mixin template JsonizeMe() { 34 import std.json : JSONValue; 35 import std.typetuple : Erase; 36 import std.traits : BaseClassesTuple; 37 38 alias T = typeof(this); 39 static if (is(T == class) && 40 __traits(hasMember, BaseClassesTuple!T[0], "populateFromJSON")) 41 { 42 override void populateFromJSON(JSONValue json) { 43 static if (!hasCustomJsonCtor!T) { 44 auto keyValPairs = json.object; 45 // check if each member is actually a member and is marked with the @jsonize attribute 46 foreach(member ; Erase!("__ctor", __traits(allMembers, T))) { 47 enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key 48 static if (key !is null) { 49 alias MemberType = typeof(mixin(member)); // deduce member type 50 auto val = extract!MemberType(keyValPairs[key]); // extract value from json 51 mixin(member ~ "= val;"); // assign value to member 52 } 53 } 54 } 55 } 56 57 override JSONValue convertToJSON() { 58 JSONValue[string] keyValPairs; 59 // look for members marked with @jsonize, ignore __ctor 60 foreach(member ; Erase!("__ctor", __traits(allMembers, T))) { 61 enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key 62 static if(key !is null) { 63 auto val = mixin(member); // get the member's value 64 keyValPairs[key] = toJSON(val); // add the pair <memberKey> : <memberValue> 65 } 66 } 67 // construct the json object 68 JSONValue json; 69 json.object = keyValPairs; 70 return json; 71 } 72 } 73 else { 74 void populateFromJSON(JSONValue json) { 75 static if (!hasCustomJsonCtor!T) { 76 auto keyValPairs = json.object; 77 // check if each member is actually a member and is marked with the @jsonize attribute 78 foreach(member ; Erase!("__ctor", __traits(allMembers, T))) { 79 enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key 80 static if (key !is null) { 81 alias MemberType = typeof(mixin(member)); // deduce member type 82 auto val = extract!MemberType(keyValPairs[key]); // extract value from json 83 mixin(member ~ "= val;"); // assign value to member 84 } 85 } 86 } 87 } 88 89 JSONValue convertToJSON() { 90 JSONValue[string] keyValPairs; 91 // look for members marked with @jsonize, ignore __ctor 92 foreach(member ; Erase!("__ctor", __traits(allMembers, T))) { 93 enum key = jsonizeKey!(this, member); // find @jsonize, deduce member key 94 static if(key !is null) { 95 auto val = mixin(member); // get the member's value 96 keyValPairs[key] = toJSON(val); // add the pair <memberKey> : <memberValue> 97 } 98 } 99 // construct the json object 100 JSONValue json; 101 json.object = keyValPairs; 102 return json; 103 } 104 } 105 } 106 107 /// json conversion of primitive types 108 unittest { 109 import std.math : approxEqual; 110 enum Category { one, two } 111 112 auto j1 = toJSON("bork"); 113 assert(j1.type == JSON_TYPE.STRING && j1.str == "bork"); 114 assert(extract!string(j1) == "bork"); 115 116 auto j2 = toJSON(4.1); 117 assert(j2.type == JSON_TYPE.FLOAT && j2.floating.approxEqual(4.1)); 118 assert(extract!float(j2).approxEqual(4.1)); 119 assert(extract!double(j2).approxEqual(4.1)); 120 assert(extract!real(j2).approxEqual(4.1)); 121 122 auto j3 = toJSON(41); 123 assert(j3.type == JSON_TYPE.INTEGER && j3.integer == 41); 124 assert(extract!int(j3) == 41); 125 assert(extract!long(j3) == 41); 126 127 auto j4 = toJSON(41u); 128 assert(j4.type == JSON_TYPE.UINTEGER && j4.uinteger == 41u); 129 assert(extract!uint(j4) == 41u); 130 assert(extract!ulong(j4) == 41u); 131 132 auto jenum = toJSON!Category(Category.one); 133 assert(jenum.type == JSON_TYPE.STRING); 134 assert(jenum.extract!Category == Category.one); 135 136 // homogenous json array 137 auto j5 = toJSON([9, 8, 7, 6]); 138 assert(j5.array[0].integer == 9); 139 assert(j5.array[1].integer == 8); 140 assert(j5.array[2].integer == 7); 141 assert(j5.array[3].integer == 6); 142 assert(j5.type == JSON_TYPE.ARRAY); 143 assert(extract!(int[])(j5) == [9, 8, 7, 6]); 144 145 // heterogenous json array 146 auto j6 = toJSON("sammich", 1.5, 2, 3u); 147 assert(j6.array[0].str == "sammich"); 148 assert(j6.array[1].floating.approxEqual(1.5)); 149 assert(j6.array[2].integer == 2); 150 assert(j6.array[3].uinteger == 3u); 151 152 // associative array 153 int[string] aa = ["a" : 1, "b" : 2, "c" : 3]; 154 auto j7 = toJSON(aa); 155 assert(j7.type == JSON_TYPE.OBJECT); 156 assert(j7.object["a"].integer == 1); 157 assert(j7.object["b"].integer == 2); 158 assert(j7.object["c"].integer == 3); 159 assert(extract!(int[string])(j7) == aa); 160 assert(j7.extract!int("b") == 2); 161 } 162 163 /// object serialization -- fields only 164 unittest { 165 import std.math : approxEqual; 166 167 static class Fields { 168 this() { } // class must have a no-args ctor 169 170 this(int iVal, float fVal, string sVal, int[] aVal, string noJson) { 171 i = iVal; 172 f = fVal; 173 s = sVal; 174 a = aVal; 175 dontJsonMe = noJson; 176 } 177 178 mixin JsonizeMe; 179 180 @jsonize { // fields to jsonize -- test different access levels 181 public int i; 182 protected float f; 183 public int[] a; 184 private string s; 185 } 186 string dontJsonMe; 187 188 override bool opEquals(Object o) { 189 auto other = cast(Fields) o; 190 return i == other.i && s == other.s && a == other.a && f.approxEqual(other.f); 191 } 192 } 193 194 auto obj = new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"); 195 auto json = toJSON!Fields(obj); 196 197 assert(json.object["i"].integer == 1); 198 assert(json.object["f"].floating.approxEqual(4.2)); 199 assert(json.object["s"].str == "tally ho!"); 200 assert(json.object["a"].array[0].integer == 9); 201 assert("dontJsonMe" !in json.object); 202 203 // reconstruct from json 204 auto r = extract!Fields(json); 205 assert(r.i == 1); 206 assert(r.f.approxEqual(4.2)); 207 assert(r.s == "tally ho!"); 208 assert(r.a == [9, 8, 7, 6]); 209 assert(r.dontJsonMe is null); 210 211 // array of objects 212 auto a = [ 213 new Fields(1, 4.2, "tally ho!", [9, 8, 7, 6], "blarg"), 214 new Fields(7, 42.2, "yea merrily", [1, 4, 6, 4], "asparagus") 215 ]; 216 217 // serialize array of user objects to json 218 auto jsonArray = toJSON!(Fields[])(a); 219 // reconstruct from json 220 assert(extract!(Fields[])(jsonArray) == a); 221 } 222 223 /// object serialization with properties 224 unittest { 225 import std.math : approxEqual; 226 227 static class Props { 228 this() { } // class must have a no-args ctor 229 230 this(int iVal, float fVal, string sVal, string noJson) { 231 _i = iVal; 232 _f = fVal; 233 _s = sVal; 234 _dontJsonMe = noJson; 235 } 236 237 mixin JsonizeMe; 238 239 @property { 240 // jsonize ref property accessing private field 241 @jsonize ref int i() { return _i; } 242 // jsonize property with non-trivial get/set methods 243 @jsonize float f() { return _f - 3; } // the jsonized value will equal _f - 3 244 float f(float val) { return _f = val + 5; } // 5 will be added to _f when retrieving from json 245 // don't jsonize these properties 246 ref string s() { return _s; } 247 ref string dontJsonMe() { return _dontJsonMe; } 248 } 249 250 private: 251 int _i; 252 float _f; 253 @jsonize string _s; 254 string _dontJsonMe; 255 } 256 257 auto obj = new Props(1, 4.2, "tally ho!", "blarg"); 258 auto json = toJSON(obj); 259 260 assert(json.object["i"].integer == 1); 261 assert(json.object["f"].floating.approxEqual(4.2 - 3.0)); // property should have subtracted 3 on retrieval 262 assert(json.object["_s"].str == "tally ho!"); 263 assert("dontJsonMe" !in json.object); 264 265 auto r = extract!Props(json); 266 assert(r.i == 1); 267 assert(r._f.approxEqual(4.2 - 3.0 + 5.0)); // property accessor should add 5 268 assert(r._s == "tally ho!"); 269 assert(r.dontJsonMe is null); 270 } 271 272 /// object serialization with custom constructor 273 unittest { 274 import std.math : approxEqual; 275 276 static class Custom { 277 mixin JsonizeMe; 278 279 this(int i) { 280 _i = i; 281 _s = "something"; 282 _f = 10.2; 283 } 284 285 @jsonize this(int _i, string _s, float _f = 20.2) { 286 this._i = _i; 287 this._s = _s ~ " jsonized"; 288 this._f = _f; 289 } 290 291 @jsonize this(double d) { // alternate ctor 292 _f = d.to!float; 293 _s = d.to!string; 294 _i = d.to!int; 295 } 296 297 private: 298 @jsonize { 299 string _s; 300 float _f; 301 int _i; 302 } 303 } 304 305 auto c = new Custom(12); 306 auto json = toJSON(c); 307 assert(json.object["_i"].integer == 12); 308 assert(json.object["_s"].str == "something"); 309 assert(json.object["_f"].floating.approxEqual(10.2)); 310 auto c2 = extract!Custom(json); 311 assert(c2._i == 12); 312 assert(c2._s == "something jsonized"); 313 assert(c2._f.approxEqual(10.2)); 314 315 // test alternate ctor 316 json = parseJSON(`{"d" : 5}`); 317 c = json.extract!Custom; 318 assert(c._f.approxEqual(5) && c._i == 5 && c._s == "5"); 319 } 320 321 /// struct serialization 322 unittest { 323 import std.math : approxEqual; 324 325 static struct S { 326 mixin JsonizeMe; 327 328 @jsonize { 329 int x; 330 float f; 331 string s; 332 } 333 int dontJsonMe; 334 335 this(int x, float f, string s, int noJson) { 336 this.x = x; 337 this.f = f; 338 this.s = s; 339 this.dontJsonMe = noJson; 340 } 341 } 342 343 auto s = S(5, 4.2, "bogus", 7); 344 auto json = toJSON(s); // serialize a struct 345 346 assert(json.object["x"].integer == 5); 347 assert(json.object["f"].floating.approxEqual(4.2)); 348 assert(json.object["s"].str == "bogus"); 349 assert("dontJsonMe" !in json.object); 350 351 auto r = extract!S(json); 352 assert(r.x == 5); 353 assert(r.f.approxEqual(4.2)); 354 assert(r.s == "bogus"); 355 assert(r.dontJsonMe == int.init); 356 } 357 358 /// json file I/O 359 unittest { 360 enum file = "test.json"; 361 scope(exit) remove(file); 362 363 static struct Data { 364 mixin JsonizeMe; 365 366 @jsonize { 367 int x; 368 string s; 369 float f; 370 } 371 } 372 373 // write an array of user-defined structs 374 auto array = [Data(5, "preposterous", 12.7), Data(8, "tesseract", -2.7), Data(5, "baby sloths", 102.7)]; 375 writeJSON(array, file); 376 auto readBack = readJSON!(Data[])(file); 377 assert(readBack == array); 378 379 // now try an associative array 380 auto aa = ["alpha": Data(27, "yams", 0), "gamma": Data(88, "spork", -99.999)]; 381 writeJSON(aa, file); 382 auto aaReadBack = readJSON!(Data[string])(file); 383 assert(aaReadBack == aa); 384 } 385 386 /// inheritance 387 unittest { 388 import std.math : approxEqual; 389 static class Parent { 390 mixin JsonizeMe; 391 @jsonize { 392 int x; 393 string s; 394 } 395 } 396 397 static class Child : Parent { 398 mixin JsonizeMe; 399 @jsonize { 400 float f; 401 } 402 } 403 404 auto c = new Child; 405 c.x = 5; 406 c.s = "hello"; 407 c.f = 2.1; 408 409 auto json = c.toJSON; 410 assert(json.extract!int("x") == 5); 411 assert(json.extract!string("s") == "hello"); 412 assert(json.extract!float("f").approxEqual(2.1)); 413 414 auto child = json.extract!Child; 415 assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1)); 416 417 auto parent = json.extract!Parent; 418 assert(parent.x == 5 && parent.s == "hello"); 419 } 420 421 /// inheritance with ctors 422 unittest { 423 import std.math : approxEqual; 424 static class Parent { 425 mixin JsonizeMe; 426 427 @jsonize this(int x, string s) { 428 _x = x; 429 _s = s; 430 } 431 432 @jsonize @property { 433 int x() { return _x; } 434 string s() { return _s; } 435 } 436 437 private: 438 int _x; 439 string _s; 440 } 441 442 static class Child : Parent { 443 mixin JsonizeMe; 444 445 @jsonize this(int x, string s, float f) { 446 super(x, s); 447 _f = f; 448 } 449 450 @jsonize @property { 451 float f() { return _f; } 452 } 453 454 private: 455 float _f; 456 } 457 458 auto c = new Child(5, "hello", 2.1); 459 460 auto json = c.toJSON; 461 assert(json.extract!int("x") == 5); 462 assert(json.extract!string("s") == "hello"); 463 assert(json.extract!float("f").approxEqual(2.1)); 464 465 auto child = json.extract!Child; 466 assert(child.x == 5 && child.s == "hello" && child.f.approxEqual(2.1)); 467 468 auto parent = json.extract!Parent; 469 assert(parent.x == 5 && parent.s == "hello"); 470 } 471 472 /// renamed members 473 unittest { 474 static class Bleh { 475 mixin JsonizeMe; 476 private { 477 @jsonize("x") int _x; 478 @jsonize("s") string _s; 479 } 480 } 481 482 auto b = new Bleh; 483 b._x = 5; 484 b._s = "blah"; 485 486 auto json = b.toJSON; 487 488 assert(json.extract!int("x") == 5); 489 assert(json.extract!string("s") == "blah"); 490 491 auto reconstruct = json.extract!Bleh; 492 assert(reconstruct._x == b._x && reconstruct._s == b._s); 493 } 494 495 // unfortunately these test classes must be implemented outside the unittest 496 // as Object.factory (and ClassInfo.find) cannot work with nested classes 497 private { 498 class TestComponent { 499 mixin JsonizeMe; 500 @jsonize int c; 501 } 502 503 class TestCompA : TestComponent { 504 mixin JsonizeMe; 505 @jsonize int a; 506 } 507 508 class TestCompB : TestComponent { 509 mixin JsonizeMe; 510 @jsonize string b; 511 } 512 } 513 514 /// type inference 515 unittest { 516 import std.traits : fullyQualifiedName; 517 518 // need to use these because unittest is assigned weird name 519 // normally would just be "modulename.classname" 520 string classKeyA = fullyQualifiedName!TestCompA; 521 string classKeyB = fullyQualifiedName!TestCompB; 522 523 assert(Object.factory(classKeyA) !is null && Object.factory(classKeyB) !is null, 524 "cannot factory classes in unittest -- this is a problem with the test"); 525 526 auto data = `[ 527 { 528 "class": "%s", 529 "c": 1, 530 "a": 5 531 }, 532 { 533 "class": "%s", 534 "c": 2, 535 "b": "hello" 536 } 537 ]`.format(classKeyA, classKeyB).parseJSON.extract!(TestComponent[]); 538 539 auto a = cast(TestCompA) data[0]; 540 auto b = cast(TestCompB) data[1]; 541 542 assert(a !is null && a.c == 1 && a.a == 5); 543 assert(b !is null && b.c == 2 && b.b == "hello"); 544 }