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