1 module graphql.helper; 2 3 import std.algorithm.searching : canFind; 4 import std.algorithm.iteration : splitter; 5 import std.format : format; 6 import std.exception : enforce, assertThrown; 7 import std.experimental.logger; 8 import std.datetime : DateTime; 9 10 import vibe.data.json; 11 12 import graphql.ast; 13 import graphql.constants; 14 15 @safe: 16 17 enum d = "data"; 18 enum e = Constants.errors; 19 20 string firstCharUpperCase(string input) { 21 import std.conv : to; 22 import std.uni : isUpper, toUpper; 23 import std.array : front, popFront; 24 if(isUpper(input.front)) { 25 return input; 26 } 27 28 const f = input.front; 29 input.popFront(); 30 31 return to!string(toUpper(f)) ~ input; 32 } 33 34 Json returnTemplate() { 35 Json ret = Json.emptyObject(); 36 ret["data"] = Json.emptyObject(); 37 ret[Constants.errors] = Json.emptyArray(); 38 return ret; 39 } 40 41 void insertError(T)(ref Json result, T t) { 42 Json tmp = Json.emptyObject(); 43 tmp["message"] = serializeToJson(t); 44 if(e !in result) { 45 result[e] = Json.emptyArray(); 46 } 47 enforce(result[e].type == Json.Type.array); 48 result[e] ~= tmp; 49 } 50 51 void insertPayload(ref Json result, string field, Json data) { 52 if(d in data) { 53 if(d !in result) { 54 result[d] = Json.emptyObject(); 55 } 56 enforce(result[d].type == Json.Type.object); 57 Json* df = field in result[d]; 58 if(df) { 59 result[d][field] = joinJson(*df, data[d]); 60 } else { 61 result[d][field] = data[d]; 62 } 63 } 64 if(e in data) { 65 if(e !in result) { 66 result[e] = Json.emptyArray(); 67 } 68 enforce(result[e].type == Json.Type.array); 69 if(!canFind(result[e].byValue(), data[e])) { 70 result[e] ~= data[e]; 71 } 72 } 73 } 74 75 unittest { 76 Json old = returnTemplate(); 77 old["data"]["foo"] = Json.emptyObject(); 78 old["data"]["foo"]["a"] = 1337; 79 80 Json n = returnTemplate(); 81 n["data"] = Json.emptyObject(); 82 n["data"]["b"] = 1338; 83 84 old.insertPayload("foo", n); 85 assert(old["data"]["foo"].length == 2, format("%s %s", 86 old["data"]["foo"].length, old.toPrettyString()) 87 ); 88 assert("a" in old["data"]["foo"]); 89 assert("b" in old["data"]["foo"]); 90 } 91 92 bool isScalar(ref const(Json) data) { 93 return data.type == Json.Type.bigInt 94 || data.type == Json.Type.bool_ 95 || data.type == Json.Type.float_ 96 || data.type == Json.Type.int_ 97 || data.type == Json.Type..string; 98 } 99 100 bool dataIsEmpty(ref const(Json) data) { 101 import std.experimental.logger; 102 if(data.type == Json.Type.object) { 103 foreach(key, value; data.byKeyValue()) { 104 if(key != Constants.errors && !value.dataIsEmpty()) { 105 //if(key != Constants.errors) { // Issue #22 place to look at 106 return false; 107 } 108 } 109 return true; 110 } else if(data.type == Json.Type.null_ 111 || data.type == Json.Type.undefined 112 ) 113 { 114 return true; 115 } else if(data.type == Json.Type.array) { 116 return data.length == 0; 117 } else if(data.type == Json.Type.bigInt 118 || data.type == Json.Type.bool_ 119 || data.type == Json.Type.float_ 120 || data.type == Json.Type.int_ 121 || data.type == Json.Type..string 122 ) 123 { 124 return false; 125 } 126 127 return true; 128 } 129 130 unittest { 131 string t = `{ "errors" : {} }`; 132 Json j = parseJsonString(t); 133 assert(j.dataIsEmpty()); 134 } 135 136 unittest { 137 string t = `{ "kind": {}, "fields": null, "name": {} }`; 138 Json j = parseJsonString(t); 139 //assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22 140 assert(j.dataIsEmpty()); 141 } 142 143 unittest { 144 string t = 145 `{ 146 "name" : { 147 "foo" : null 148 } 149 }`; 150 Json j = parseJsonString(t); 151 //assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22 152 assert(j.dataIsEmpty()); 153 } 154 155 bool dataIsNull(ref const(Json) data) { 156 import std.format : format; 157 enforce(data.type == Json.Type.object, format("%s", data)); 158 if(const(Json)* d = "data" in data) { 159 return d.type == Json.Type.null_; 160 } 161 return false; 162 } 163 164 Json getWithPath(Json input, string path) { 165 auto sp = path.splitter("."); 166 foreach(s; sp) { 167 Json* n = s in input; 168 enforce(n !is null, "failed to traverse the input at " ~ s); 169 input = *n; 170 } 171 return input; 172 } 173 174 unittest { 175 string t = 176 `{ 177 "name" : { 178 "foo" : 13 179 } 180 }`; 181 Json j = parseJsonString(t); 182 Json f = j.getWithPath("name"); 183 assert("foo" in f); 184 185 f = j.getWithPath("name.foo"); 186 enforce(f.to!int() == 13); 187 188 assertThrown(j.getWithPath("doesnotexist")); 189 assertThrown(j.getWithPath("name.alsoNotThere")); 190 } 191 192 enum JoinJsonPrecedence { 193 none, 194 a, 195 b 196 } 197 198 /** Merge two Json objects. 199 Values in a take precedence over values in b. 200 */ 201 Json joinJson(JoinJsonPrecedence jjp = JoinJsonPrecedence.none)(Json a, Json b) 202 { 203 // we can not merge null or undefined values 204 if(a.type == Json.Type.null_ || a.type == Json.Type.undefined) { 205 return b; 206 } 207 if(b.type == Json.Type.null_ || b.type == Json.Type.undefined) { 208 return a; 209 } 210 211 // we need objects to merge 212 if(a.type == Json.Type.object && b.type == Json.Type.object) { 213 Json ret = a.clone(); 214 foreach(key, value; b.byKeyValue()) { 215 Json* ap = key in ret; 216 if(ap is null) { 217 ret[key] = value; 218 } else if(ap.type == Json.Type.object 219 && value.type == Json.Type.object) 220 { 221 ret[key] = joinJson(*ap, value); 222 } else { 223 static if(jjp == JoinJsonPrecedence.none) { 224 throw new Exception(format( 225 "Can not join '%s' and '%s' on key '%s'", 226 ap.type, value.type, key)); 227 } else static if(jjp == JoinJsonPrecedence.a) { 228 } else { 229 ret[key] = value; 230 } 231 } 232 } 233 return ret; 234 } 235 return a; 236 } 237 238 unittest { 239 Json a = parseJsonString(`{"overSize":200}`); 240 Json b = parseJsonString(`{}`); 241 const c = joinJson(b, a); 242 assert(c == a); 243 244 b = parseJsonString(`{"underSize":-100}`); 245 const d = joinJson(b, a); 246 Json r = parseJsonString(`{"overSize":200, "underSize":-100}`); 247 assert(d == r); 248 } 249 250 unittest { 251 Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 252 parseJsonString(`{"underSize": {"b": 100}}`) 253 ); 254 255 Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`); 256 assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString())); 257 } 258 259 unittest { 260 assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 261 parseJsonString(`{"underSize": {"a": 100}}`) 262 )); 263 } 264 265 unittest { 266 assertThrown(joinJson(parseJsonString(`{"underSize": -100}`), 267 parseJsonString(`{"underSize": {"a": 100}}`) 268 )); 269 } 270 271 template toType(T) { 272 import std.bigint : BigInt; 273 import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint, 274 isSomeString; 275 static if(is(T == bool)) { 276 enum toType = Json.Type.bool_; 277 } else static if(isIntegral!(T)) { 278 enum toType = Json.Type.int_; 279 } else static if(isFloatingPoint!(T)) { 280 enum toType = Json.Type.float_; 281 } else static if(isSomeString!(T)) { 282 enum toType = Json.Type..string; 283 } else static if(isArray!(T)) { 284 enum toType = Json.Type.array; 285 } else static if(isAggregateType!(T)) { 286 enum toType = Json.Type.object; 287 } else static if(is(T == BigInt)) { 288 enum toType = Json.Type.bigint; 289 } else { 290 enum toType = Json.Type.undefined; 291 } 292 } 293 294 bool hasPathTo(T)(Json data, string path, ref T ret) { 295 enum TT = toType!T; 296 auto sp = path.splitter("."); 297 string f; 298 while(!sp.empty) { 299 f = sp.front; 300 sp.popFront(); 301 if(data.type != Json.Type.object || f !in data) { 302 return false; 303 } else { 304 data = data[f]; 305 } 306 } 307 static if(is(T == Json)) { 308 ret = data; 309 return true; 310 } else { 311 if(data.type == TT) { 312 ret = data.to!T(); 313 return true; 314 } 315 return false; 316 } 317 } 318 319 unittest { 320 Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`); 321 Json ret; 322 assert(hasPathTo!Json(d, "foo", ret)); 323 } 324 325 /** 326 params: 327 path = A "." seperated path 328 */ 329 T getWithDefault(T)(Json data, string[] paths...) { 330 enum TT = toType!T; 331 T ret = T.init; 332 foreach(string path; paths) { 333 if(hasPathTo!T(data, path, ret)) { 334 return ret; 335 } 336 } 337 return ret; 338 } 339 340 unittest { 341 Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8, 342 "__typename":"Starship","series":["DeepSpaceNine", 343 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 344 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`); 345 const r = d.getWithDefault!string("data.__typename"); 346 assert(r == "Starship", r); 347 } 348 349 unittest { 350 Json d = parseJsonString(`{"commanderId":8, 351 "__typename":"Starship","series":["DeepSpaceNine", 352 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 353 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 354 const r = d.getWithDefault!string("data.__typename", "__typename"); 355 assert(r == "Starship", r); 356 } 357 358 unittest { 359 Json d = parseJsonString(`{"commanderId":8, 360 "__typename":"Starship","series":["DeepSpaceNine", 361 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 362 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 363 const r = d.getWithDefault!string("__typename"); 364 assert(r == "Starship", r); 365 } 366 367 // TODO should return ref 368 auto accessNN(string[] tokens,T)(T tmp0) { 369 import std.array : back; 370 import std.format : format; 371 if(tmp0 !is null) { 372 static foreach(idx, token; tokens) { 373 mixin(format! 374 `if(tmp%d is null) return null; 375 auto tmp%d = tmp%d.%s;`(idx, idx+1, idx, token) 376 ); 377 } 378 return mixin(format("tmp%d", tokens.length)); 379 } 380 return null; 381 } 382 383 unittest { 384 class A { 385 int a; 386 } 387 388 class B { 389 A a; 390 } 391 392 class C { 393 B b; 394 } 395 396 auto c1 = new C; 397 assert(c1.accessNN!(["b", "a"]) is null); 398 399 c1.b = new B; 400 assert(c1.accessNN!(["b"]) !is null); 401 402 assert(c1.accessNN!(["b", "a"]) is null); 403 // TODO not sure why this is not a lvalue 404 //c1.accessNN!(["b", "a"]) = new A; 405 c1.b.a = new A; 406 assert(c1.accessNN!(["b", "a"]) !is null); 407 } 408 409 T extract(T)(Json data, string name) { 410 enforce(data.type == Json.Type.object, format! 411 "Trying to get a '%s' by name '%s' but passed Json is not an object" 412 (T.stringof, name) 413 ); 414 415 Json* item = name in data; 416 417 enforce(item !is null, format!( 418 "Trying to get a '%s' by name '%s' which is not present in passed " 419 ~ "object '%s'" 420 )(T.stringof, name, data) 421 ); 422 423 return (*item).to!T(); 424 } 425 426 unittest { 427 import std.exception : assertThrown; 428 Json j = parseJsonString(`{ "foo": 1337 }`); 429 auto foo = j.extract!int("foo"); 430 431 assertThrown(Json.emptyObject().extract!float("Hello")); 432 assertThrown(j.extract!string("Hello")); 433 } 434 435 const(Document) lexAndParse(string s) { 436 import graphql.lexer; 437 import graphql.parser; 438 auto l = Lexer(s, QueryParser.no); 439 auto p = Parser(l); 440 const(Document) doc = p.parseDocument(); 441 return doc; 442 } 443 444 string stringTypeStrip(string str) { 445 import std.algorithm.searching : startsWith, endsWith, canFind; 446 import std.string : capitalize, indexOf, strip; 447 immutable fs = ["Nullable!", "NullableStore!", "GQLDCustomLeaf!"]; 448 immutable arr = "[]"; 449 outer: while(true) { 450 str = str.strip(); 451 foreach(f; fs) { 452 if(str.startsWith(f)) { 453 str = str[f.length .. $]; 454 continue outer; 455 } 456 } 457 if(str.endsWith(arr)) { 458 str = str[0 .. str.length - arr.length]; 459 continue; 460 } else if(str.endsWith("!")) { 461 str = str[0 .. $ - 1]; 462 continue; 463 } else if(str.startsWith("[")) { 464 str = str[1 .. $]; 465 continue; 466 } else if(str.endsWith("]")) { 467 str = str[0 .. $ - 1]; 468 continue; 469 } else if(str.startsWith("(")) { 470 str = str[1 .. $]; 471 continue; 472 } else if(str.endsWith(")")) { 473 str = str[0 .. $ - 1]; 474 continue; 475 } else if(str.endsWith("'")) { 476 str = str[0 .. $ - 1]; 477 continue; 478 } 479 break; 480 } 481 482 const comma = str.indexOf(','); 483 str = comma == -1 ? str : str[0 .. comma].strip(); 484 485 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 486 ? "Int" 487 : str; 488 489 str = canFind(["string", "int", "float", "bool"], str) 490 ? capitalize(str) 491 : str; 492 493 str = str == "__type" ? "__Type" : str; 494 str = str == "__schema" ? "__Schema" : str; 495 str = str == "__inputvalue" ? "__InputValue" : str; 496 str = str == "__directive" ? "__Directive" : str; 497 str = str == "__field" ? "__Field" : str; 498 499 return str; 500 } 501 502 unittest { 503 string t = "Nullable!string"; 504 string r = t.stringTypeStrip(); 505 assert(r == "String", r); 506 507 t = "Nullable!(string[])"; 508 r = t.stringTypeStrip(); 509 assert(r == "String", r); 510 } 511 512 unittest { 513 string t = "Nullable!__type"; 514 string r = t.stringTypeStrip(); 515 assert(r == "__Type", r); 516 517 t = "Nullable!(__type[])"; 518 r = t.stringTypeStrip(); 519 assert(r == "__Type", r); 520 } 521 522 Json toGraphqlJson(T)(auto ref T input) { 523 import std.typecons : Nullable; 524 import std.array : empty; 525 import std.traits : isArray, isAggregateType, isBasicType, isSomeString, 526 isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple; 527 528 import nullablestore; 529 530 import graphql.uda : GQLDCustomLeaf; 531 static if(isArray!T && !isSomeString!T) { 532 Json ret = Json.emptyArray(); 533 foreach(ref it; input) { 534 ret ~= toGraphqlJson(it); 535 } 536 return ret; 537 } else static if(is(T : GQLDCustomLeaf!Type, Type...)) { 538 return Json(Type[1](input)); 539 } else static if(is(T : Nullable!Type, Type)) { 540 return input.isNull() ? Json(null) : toGraphqlJson(input.get()); 541 } else static if(isBasicType!T || isScalarType!T || isSomeString!T) { 542 return serializeToJson(input); 543 } else static if(isAggregateType!T) { 544 Json ret = Json.emptyObject(); 545 546 // the important bit is the setting of the __typename field 547 ret["__typename"] = T.stringof; 548 alias names = FieldNameTuple!(T); 549 alias types = FieldTypeTuple!(T); 550 static foreach(idx; 0 .. names.length) {{ 551 static if(!names[idx].empty) { 552 static if(is(types[idx] : NullableStore!Type, Type)) { 553 } else static if(is(types[idx] == enum)) { 554 ret[names[idx]] = serializeToJson( 555 __traits(getMember, input, names[idx]) 556 ); 557 } else { 558 ret[names[idx]] = toGraphqlJson( 559 __traits(getMember, input, names[idx]) 560 ); 561 } 562 } 563 }} 564 return ret; 565 } else { 566 static assert(false, T.stringof ~ " not supported"); 567 } 568 } 569 570 /*Json toGraphqlJson(T)(auto ref T obj) { 571 import graphql.uda : GQLDCustomLeaf; 572 import std.traits : isArray, isSomeString, FieldNameTuple, FieldTypeTuple; 573 import std.array : empty; 574 import std.typecons : Nullable; 575 import nullablestore; 576 577 alias names = FieldNameTuple!(T); 578 alias types = FieldTypeTuple!(T); 579 580 static if(isArray!T && !isSomeString!T) { 581 Json ret = Json.emptyArray(); 582 foreach(ref it; obj) { 583 ret ~= toGraphqlJson(it); 584 } 585 } else static if(is(T : GQLDCustomLeaf!Type, Type...)) { 586 Json ret = Json(Type[1](obj)); 587 } else { 588 Json ret = Json.emptyObject(); 589 590 // the important bit is the setting of the __typename field 591 ret["__typename"] = T.stringof; 592 593 static foreach(idx; 0 .. names.length) {{ 594 static if(!names[idx].empty) { 595 //writefln("%s %s", __LINE__, names[idx]); 596 static if(is(types[idx] : Nullable!Type, Type)) { 597 if(__traits(getMember, obj, names[idx]).isNull()) { 598 ret[names[idx]] = Json(null); 599 } else { 600 ret[names[idx]] = toGraphqlJson( 601 __traits(getMember, obj, names[idx]).get() 602 ); 603 } 604 } else static if(is(types[idx] : GQLDCustomLeaf!Type, Type...)) { 605 ret[names[idx]] = Json(Type[1]( 606 __traits(getMember, obj, names[idx])) 607 ); 608 } else static if(is(types[idx] : NullableStore!Type, Type)) { 609 } else { 610 ret[names[idx]] = serializeToJson( 611 __traits(getMember, obj, names[idx]) 612 ); 613 } 614 } 615 }} 616 } 617 return ret; 618 }*/ 619 620 string dtToString(DateTime dt) { 621 return dt.toISOExtString(); 622 } 623 624 unittest { 625 import std.typecons : nullable, Nullable; 626 import graphql.uda; 627 import nullablestore; 628 629 struct Foo { 630 int a; 631 Nullable!int b; 632 NullableStore!float c; 633 GQLDCustomLeaf!(DateTime, dtToString) dt2; 634 Nullable!(GQLDCustomLeaf!(DateTime, dtToString)) dt; 635 } 636 637 DateTime dt = DateTime(1337, 7, 1, 1, 1, 1); 638 DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3); 639 640 Foo foo; 641 foo.dt2 = GQLDCustomLeaf!(DateTime, dtToString)(dt2); 642 foo.dt = nullable(GQLDCustomLeaf!(DateTime, dtToString)(dt)); 643 Json j = toGraphqlJson(foo); 644 assert(j["a"].to!int() == 0); 645 assert(j["b"].type == Json.Type.null_); 646 assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type, 647 j.toPrettyString() 648 ) 649 ); 650 string exp = j["dt"].to!string(); 651 assert(exp == "1337-07-01T01:01:01", exp); 652 string exp2 = j["dt2"].to!string(); 653 assert(exp2 == "2337-07-01T01:01:03", exp2); 654 }