1 /* 2 *Copyright (C) 2018 Laurent Tréguier 3 * 4 *This file is part of DLS. 5 * 6 *DLS is free software: you can redistribute it and/or modify 7 *it under the terms of the GNU General Public License as published by 8 *the Free Software Foundation, either version 3 of the License, or 9 *(at your option) any later version. 10 * 11 *DLS is distributed in the hope that it will be useful, 12 *but WITHOUT ANY WARRANTY; without even the implied warranty of 13 *MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 *GNU General Public License for more details. 15 * 16 *You should have received a copy of the GNU General Public License 17 *along with DLS. If not, see <http://www.gnu.org/licenses/>. 18 * 19 */ 20 21 module dls.util.json; 22 23 import std.json : JSONValue; 24 import std.traits : isArray, isAssociativeArray, isBoolean, isNumeric, 25 isSomeChar, isSomeString; 26 import std.typecons : Nullable; 27 28 /++ 29 Converts a `JSONValue` to an object of type `T` by filling its fields with the JSON's fields. 30 +/ 31 T convertFromJSON(T)(JSONValue json) if (is(T == class) || is(T == struct)) 32 { 33 import std.json : JSONException, JSON_TYPE; 34 import std.traits : isSomeFunction, isType; 35 36 static if (is(T == class)) 37 { 38 auto result = new T(); 39 } 40 else 41 { 42 auto result = T(); 43 } 44 45 if (json.type != JSON_TYPE.OBJECT) 46 { 47 throw new JSONException(json.toString() ~ " is not an object type"); 48 } 49 50 foreach (member; __traits(allMembers, T)) 51 { 52 static if (__traits(getProtection, __traits(getMember, T, 53 member)) == "public" && !isType!(__traits(getMember, T, 54 member)) && !isSomeFunction!(__traits(getMember, T, member))) 55 { 56 try 57 { 58 __traits(getMember, result, member) = convertFromJSON!(typeof(__traits(getMember, 59 result, member)))(json[normalizeMemberName(member)]); 60 } 61 catch (JSONException e) 62 { 63 } 64 } 65 } 66 67 return result; 68 } 69 70 version (unittest) 71 { 72 struct TestStruct 73 { 74 uint uinteger; 75 JSONValue json; 76 } 77 78 class TestClass 79 { 80 int integer; 81 float floating; 82 string text; 83 int[] array; 84 string[string] dictionary; 85 TestStruct testStruct; 86 } 87 } 88 89 unittest 90 { 91 import std.json : parseJSON; 92 93 const jsonString = `{ 94 "integer": 42, 95 "floating": 3.0, 96 "text": "Hello world", 97 "array": [0, 1, 2], 98 "dictionary": { 99 "key1": "value1", 100 "key2": "value2", 101 "key3": "value3" 102 }, 103 "testStruct": { 104 "uinteger": 16, 105 "json": { 106 "key": "value" 107 } 108 } 109 }`; 110 111 const testClass = convertFromJSON!TestClass(parseJSON(jsonString)); 112 assert(testClass.integer == 42); 113 assert(testClass.floating == 3.0); 114 assert(testClass.text == "Hello world"); 115 assert(testClass.array == [0, 1, 2]); 116 const dictionary = ["key1" : "value1", "key2" : "value2", "key3" : "value3"]; 117 assert(testClass.dictionary == dictionary); 118 assert(testClass.testStruct.uinteger == 16); 119 assert(testClass.testStruct.json["key"].str == "value"); 120 } 121 122 N convertFromJSON(N : Nullable!T, T)(JSONValue json) 123 { 124 import std.json : JSON_TYPE; 125 import std.typecons : nullable; 126 127 return (json.type == JSON_TYPE.NULL) ? N() : convertFromJSON!T(json).nullable; 128 } 129 130 unittest 131 { 132 auto json = JSONValue(42); 133 auto result = convertFromJSON!(Nullable!int)(json); 134 assert(!result.isNull && result.get() == json.integer); 135 136 json = JSONValue(null); 137 assert(convertFromJSON!(Nullable!int)(json).isNull); 138 } 139 140 T convertFromJSON(T : JSONValue)(JSONValue json) 141 { 142 import std.typecons : nullable; 143 144 return json.nullable; 145 } 146 147 unittest 148 { 149 assert(convertFromJSON!JSONValue(JSONValue(42)) == JSONValue(42)); 150 } 151 152 T convertFromJSON(T)(JSONValue json) if (isNumeric!T || isSomeChar!T) 153 { 154 import std.conv : to; 155 import std.json : JSONException, JSON_TYPE; 156 157 switch (json.type) 158 { 159 case JSON_TYPE.NULL, JSON_TYPE.FALSE: 160 return 0.to!T; 161 162 case JSON_TYPE.TRUE: 163 return 1.to!T; 164 165 case JSON_TYPE.FLOAT: 166 return json.floating.to!T; 167 168 case JSON_TYPE.INTEGER: 169 return json.integer.to!T; 170 171 case JSON_TYPE.UINTEGER: 172 return json.uinteger.to!T; 173 174 case JSON_TYPE.STRING: 175 return json.str.to!T; 176 177 default: 178 throw new JSONException(json.toString() ~ " is not a numeric type"); 179 } 180 } 181 182 unittest 183 { 184 assert(convertFromJSON!float(JSONValue(3.0)) == 3.0); 185 assert(convertFromJSON!int(JSONValue(42)) == 42); 186 assert(convertFromJSON!uint(JSONValue(42U)) == 42U); 187 assert(convertFromJSON!char(JSONValue('a')) == 'a'); 188 189 // quirky JSON cases 190 191 assert(convertFromJSON!int(JSONValue(null)) == 0); 192 assert(convertFromJSON!int(JSONValue(false)) == 0); 193 assert(convertFromJSON!int(JSONValue(true)) == 1); 194 assert(convertFromJSON!int(JSONValue("42")) == 42); 195 assert(convertFromJSON!char(JSONValue("a")) == 'a'); 196 } 197 198 T convertFromJSON(T)(JSONValue json) if (isBoolean!T) 199 { 200 import std.json : JSON_TYPE; 201 202 switch (json.type) 203 { 204 case JSON_TYPE.NULL, JSON_TYPE.FALSE: 205 return false; 206 207 case JSON_TYPE.FLOAT: 208 return json.floating != 0; 209 210 case JSON_TYPE.INTEGER: 211 return json.integer != 0; 212 213 case JSON_TYPE.UINTEGER: 214 return json.uinteger != 0; 215 216 case JSON_TYPE.STRING: 217 return json.str.length > 0; 218 219 default: 220 return true; 221 } 222 } 223 224 unittest 225 { 226 assert(convertFromJSON!bool(JSONValue(false)) == false); 227 assert(convertFromJSON!bool(JSONValue(true)) == true); 228 229 // quirky JSON cases 230 231 assert(convertFromJSON!bool(JSONValue(null)) == false); 232 assert(convertFromJSON!bool(JSONValue(0.0)) == false); 233 assert(convertFromJSON!bool(JSONValue(0)) == false); 234 assert(convertFromJSON!bool(JSONValue(0U)) == false); 235 assert(convertFromJSON!bool(JSONValue("")) == false); 236 237 assert(convertFromJSON!bool(JSONValue(3.0)) == true); 238 assert(convertFromJSON!bool(JSONValue(42)) == true); 239 assert(convertFromJSON!bool(JSONValue(42U)) == true); 240 assert(convertFromJSON!bool(JSONValue("Hello world")) == true); 241 assert(convertFromJSON!bool(JSONValue(new int[0])) == true); 242 } 243 244 T convertFromJSON(T)(JSONValue json) 245 if (isSomeString!T || is(T : string) || is(T : wstring) || is(T : dstring)) 246 { 247 import std.conv : to; 248 import std.json : JSONException, JSON_TYPE; 249 250 static if (is(T == enum)) 251 { 252 foreach (member; __traits(allMembers, T)) 253 { 254 auto m = __traits(getMember, T, member); 255 256 if (json.str == m) 257 { 258 return m; 259 } 260 } 261 262 throw new JSONException(json.toString() ~ " is not a member of " ~ typeid(T).toString()); 263 } 264 else 265 { 266 return (json.type == JSON_TYPE.STRING ? json.str : json.toString()).to!T; 267 } 268 } 269 270 unittest 271 { 272 enum Operation : string 273 { 274 create = "create", 275 delete_ = "delete" 276 } 277 278 assert(convertFromJSON!Operation(JSONValue("create")) == Operation.create); 279 assert(convertFromJSON!Operation(JSONValue("delete")) == Operation.delete_); 280 281 auto json = JSONValue("Hello"); 282 assert(convertFromJSON!string(json) == json.str); 283 assert(convertFromJSON!(char[])(json) == json.str); 284 assert(convertFromJSON!(wstring)(json) == "Hello"w); 285 assert(convertFromJSON!(wchar[])(json) == "Hello"w); 286 assert(convertFromJSON!(dstring)(json) == "Hello"d); 287 assert(convertFromJSON!(dchar[])(json) == "Hello"d); 288 289 // beware of the fact that JSONValue treats chars as integers; this returns "97" and not "a" 290 assert(convertFromJSON!string(JSONValue('a')) != "a"); 291 assert(convertFromJSON!string(JSONValue("a")) == "a"); 292 293 enum TestEnum : string 294 { 295 hello = "hello", 296 world = "world" 297 } 298 299 assert(convertFromJSON!TestEnum(JSONValue("hello")) == TestEnum.hello); 300 assert(convertFromJSON!TestEnum(JSONValue("world")) == TestEnum.world); 301 302 // quirky JSON cases 303 304 assert(convertFromJSON!string(JSONValue(null)) == "null"); 305 assert(convertFromJSON!string(JSONValue(false)) == "false"); 306 assert(convertFromJSON!string(JSONValue(true)) == "true"); 307 } 308 309 T convertFromJSON(T : U[], U)(JSONValue json) 310 if (isArray!T && !isSomeString!T && !is(T : string) && !is(T : wstring) && !is(T : dstring)) 311 { 312 import std.algorithm : map; 313 import std.array : array; 314 import std.conv : to; 315 import std.json : JSONException, JSON_TYPE; 316 317 switch (json.type) 318 { 319 case JSON_TYPE.NULL: 320 return []; 321 322 case JSON_TYPE.FALSE: 323 return [convertFromJSON!U(JSONValue(false))]; 324 325 case JSON_TYPE.TRUE: 326 return [convertFromJSON!U(JSONValue(true))]; 327 328 case JSON_TYPE.ARRAY: 329 return json.array 330 .map!(value => convertFromJSON!U(value)) 331 .array 332 .to!T; 333 334 case JSON_TYPE.OBJECT: 335 throw new JSONException(json.toString() ~ " is not a string type"); 336 337 default: 338 return [convertFromJSON!U(json)]; 339 } 340 } 341 342 unittest 343 { 344 assert(convertFromJSON!(int[])(JSONValue([0, 1, 2, 3])) == [0, 1, 2, 3]); 345 346 // quirky JSON cases 347 348 assert(convertFromJSON!(int[])(JSONValue(null)) == []); 349 assert(convertFromJSON!(int[])(JSONValue(false)) == [0]); 350 assert(convertFromJSON!(bool[])(JSONValue(true)) == [true]); 351 assert(convertFromJSON!(float[])(JSONValue(3.0)) == [3.0]); 352 assert(convertFromJSON!(int[])(JSONValue(42)) == [42]); 353 assert(convertFromJSON!(uint[])(JSONValue(42U)) == [42U]); 354 assert(convertFromJSON!(string[])(JSONValue("Hello")) == ["Hello"]); 355 } 356 357 T convertFromJSON(T : U[K], U, K)(JSONValue json) if (isAssociativeArray!T) 358 { 359 import std.conv : to; 360 import std.json : JSONException, JSON_TYPE; 361 362 U[K] result; 363 364 switch (json.type) 365 { 366 case JSON_TYPE.NULL: 367 return result; 368 369 case JSON_TYPE.OBJECT: 370 foreach (key, value; json.object) 371 { 372 result[key.to!K] = convertFromJSON!U(value); 373 } 374 375 break; 376 377 case JSON_TYPE.ARRAY: 378 foreach (key, value; json.array) 379 { 380 result[key.to!K] = convertFromJSON!U(value); 381 } 382 383 break; 384 385 default: 386 throw new JSONException(json.toString() ~ " is not an object type"); 387 } 388 389 return result; 390 } 391 392 unittest 393 { 394 auto dictionary = ["hello" : 42, "world" : 0]; 395 assert(convertFromJSON!(int[string])(JSONValue(dictionary)) == dictionary); 396 397 // quirky JSON cases 398 399 assert(convertFromJSON!(int[string])(JSONValue([16, 42])) == ["0" : 16, "1" : 42]); 400 dictionary.clear(); 401 assert(convertFromJSON!(int[string])(JSONValue(null)) == dictionary); 402 } 403 404 Nullable!JSONValue convertToJSON(T)(T value) 405 if ((is(T == class) || is(T == struct)) && !is(T == JSONValue)) 406 { 407 import std.traits : isSomeFunction, isType; 408 import std.typecons : nullable; 409 410 static if (is(T == class)) 411 { 412 if (value is null) 413 { 414 return JSONValue(null).nullable; 415 } 416 } 417 418 auto result = JSONValue(); 419 420 foreach (member; __traits(allMembers, T)) 421 { 422 static if (__traits(getProtection, __traits(getMember, T, 423 member)) == "public" && !isType!(__traits(getMember, T, 424 member)) && !isSomeFunction!(__traits(getMember, T, member))) 425 { 426 auto json = convertToJSON!(typeof(__traits(getMember, value, member)))( 427 __traits(getMember, value, member)); 428 429 if (!json.isNull) 430 { 431 result[normalizeMemberName(member)] = json.get(); 432 } 433 } 434 } 435 436 return result.nullable; 437 } 438 439 unittest 440 { 441 import std.json : parseJSON; 442 443 auto testClass = new TestClass(); 444 testClass.integer = 42; 445 testClass.floating = 3.5; 446 testClass.text = "Hello world"; 447 testClass.array = [0, 1, 2]; 448 testClass.dictionary = ["key1" : "value1", "key2" : "value2"]; 449 testClass.testStruct = TestStruct(); 450 testClass.testStruct.uinteger = 16; 451 testClass.testStruct.json = JSONValue(["key1" : "value1", "key2" : "value2"]); 452 453 auto jsonString = `{ 454 "integer": 42, 455 "floating": 3.5, 456 "text": "Hello world", 457 "array": [0, 1, 2], 458 "dictionary": { 459 "key1": "value1", 460 "key2": "value2" 461 }, 462 "testStruct": { 463 "uinteger": 16, 464 "json": { 465 "key1": "value1", 466 "key2": "value2" 467 } 468 } 469 }`; 470 471 auto json = convertToJSON(testClass); 472 // parseJSON() will parse `uinteger` as a regular integer, meaning that the JSON's aren't considered equal, 473 // even though technically they are equivalent (16 as int or as uint is technically the same value), which 474 // is why .toString() is used here 475 assert(json.get().toString() == parseJSON(jsonString).toString()); 476 477 TestClass nullTestClass = null; 478 auto nullJson = convertToJSON(nullTestClass); 479 assert(!nullJson.isNull && nullJson.get().isNull); 480 } 481 482 Nullable!JSONValue convertToJSON(N : Nullable!T, T)(N value) 483 { 484 return value.isNull ? Nullable!JSONValue() : convertToJSON!T(value.get()); 485 } 486 487 unittest 488 { 489 assert(convertToJSON(Nullable!int()) == Nullable!JSONValue()); 490 assert(convertToJSON(Nullable!int(42)) == JSONValue(42)); 491 } 492 493 Nullable!JSONValue convertToJSON(T)(T value) 494 if ((!is(T == class) && !is(T == struct)) || is(T == JSONValue)) 495 { 496 import std.typecons : nullable; 497 498 return JSONValue(value).nullable; 499 } 500 501 unittest 502 { 503 assert(convertToJSON(3.0) == JSONValue(3.0)); 504 assert(convertToJSON(42) == JSONValue(42)); 505 assert(convertToJSON(42U) == JSONValue(42U)); 506 assert(convertToJSON(false) == JSONValue(false)); 507 assert(convertToJSON(true) == JSONValue(true)); 508 assert(convertToJSON('a') == JSONValue('a')); 509 assert(convertToJSON("Hello world") == JSONValue("Hello world")); 510 assert(convertToJSON(JSONValue(42)) == JSONValue(42)); 511 } 512 513 Nullable!JSONValue convertToJSON(T : U[], U)(T value) 514 if (isArray!T && !isSomeString!T && !is(T : string) && !is(T : wstring) && !is(T : dstring)) 515 { 516 import std.algorithm : map; 517 import std.array : array; 518 import std.typecons : nullable; 519 520 return JSONValue(value.map!(item => convertToJSON(item))() 521 .map!(json => json.isNull ? JSONValue(null) : json).array).nullable; 522 } 523 524 unittest 525 { 526 assert(convertToJSON([0, 1, 2]) == JSONValue([0, 1, 2])); 527 assert(convertToJSON(["hello", "world"]) == JSONValue(["hello", "world"])); 528 } 529 530 Nullable!JSONValue convertToJSON(T : U[K], U, K)(T value) if (isAssociativeArray!T) 531 { 532 import std.conv : to; 533 import std.typecons : nullable; 534 535 auto result = JSONValue(); 536 537 foreach (key; value.keys) 538 { 539 auto json = convertToJSON(value[key]); 540 result[key.to!string] = json.isNull ? JSONValue(null) : json; 541 } 542 543 return result.nullable; 544 } 545 546 unittest 547 { 548 assert(convertToJSON(["hello" : 16, "world" : 42]) == JSONValue(["hello" : 16, "world" : 42])); 549 assert(convertToJSON(['a' : 16, 'b' : 42]) == JSONValue(["a" : 16, "b" : 42])); 550 assert(convertToJSON([0 : 16, 1 : 42]) == JSONValue(["0" : 16, "1" : 42])); 551 } 552 553 /++ 554 Removes underscores from names. Some protocol variable names can be reserved names (like `version`) and thus have an 555 added underscore in their protocol definition. 556 +/ 557 private string normalizeMemberName(string name) 558 { 559 import std..string : endsWith; 560 561 return name.endsWith('_') ? name[0 .. $ - 1] : name; 562 } 563 564 unittest 565 { 566 assert(normalizeMemberName("hello") == "hello"); 567 assert(normalizeMemberName("hello_") == "hello"); 568 assert(normalizeMemberName("_hello") == "_hello"); 569 assert(normalizeMemberName("hel_lo") == "hel_lo"); 570 }