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