1 module graphql.helper; 2 3 import std.array : empty; 4 import std.algorithm.iteration : each, splitter; 5 import std.algorithm.searching : startsWith, endsWith, canFind; 6 import std.conv : to; 7 import std.datetime : DateTime, Date; 8 import std.exception : enforce, assertThrown; 9 import std.format : format; 10 import std.stdio; 11 import std.string : capitalize, indexOf, strip; 12 import std.typecons : nullable, Nullable; 13 14 import vibe.data.json; 15 16 import graphql.ast; 17 import graphql.uda; 18 import graphql.constants; 19 import graphql.exception; 20 21 @safe: 22 23 /** dmd and ldc have problems with generation all functions 24 This functions call functions that were undefined. 25 */ 26 private void undefinedFunctions() @trusted { 27 static import core.internal.hash; 28 static import graphql.schema.introspectiontypes; 29 30 const(graphql.schema.introspectiontypes.__Type)[] tmp; 31 core.internal.hash.hashOf!(const(graphql.schema.introspectiontypes.__Type)[]) 32 (tmp, 0); 33 } 34 35 enum d = "data"; 36 enum e = Constants.errors; 37 38 string firstCharUpperCase(string input) { 39 import std.conv : to; 40 import std.uni : isUpper, toUpper; 41 import std.array : front, popFront; 42 if(isUpper(input.front)) { 43 return input; 44 } 45 46 const f = input.front; 47 input.popFront(); 48 49 return to!string(toUpper(f)) ~ input; 50 } 51 52 Json returnTemplate() { 53 Json ret = Json.emptyObject(); 54 ret["data"] = Json.emptyObject(); 55 ret[Constants.errors] = Json.emptyArray(); 56 return ret; 57 } 58 59 void insertError(T)(ref Json result, T t) { 60 insertError(result, t, []); 61 } 62 63 void insertError(T)(ref Json result, T t, PathElement[] path) { 64 Json tmp = Json.emptyObject(); 65 tmp["message"] = serializeToJson(t); 66 if(!path.empty) { 67 tmp["path"] = Json.emptyArray(); 68 foreach(it; path) { 69 tmp["path"] ~= it.toJson(); 70 } 71 } 72 if(e !in result) { 73 result[e] = Json.emptyArray(); 74 } 75 enforce(result[e].type == Json.Type.array); 76 result[e] ~= tmp; 77 } 78 79 void insertPayload(ref Json result, string field, Json data) { 80 if(d in data) { 81 if(d !in result) { 82 result[d] = Json.emptyObject(); 83 } 84 enforce(result[d].type == Json.Type.object); 85 Json* df = field in result[d]; 86 if(df) { 87 result[d][field] = joinJson(*df, data[d]); 88 } else { 89 result[d][field] = data[d]; 90 } 91 } 92 if(e in data) { 93 if(e !in result) { 94 result[e] = Json.emptyArray(); 95 } 96 enforce(result[e].type == Json.Type.array); 97 if(!canFind(result[e].byValue(), data[e])) { 98 result[e] ~= data[e]; 99 } 100 } 101 } 102 103 unittest { 104 Json old = returnTemplate(); 105 old["data"]["foo"] = Json.emptyObject(); 106 old["data"]["foo"]["a"] = 1337; 107 108 Json n = returnTemplate(); 109 n["data"] = Json.emptyObject(); 110 n["data"]["b"] = 1338; 111 112 old.insertPayload("foo", n); 113 assert(old["data"]["foo"].length == 2, format("%s %s", 114 old["data"]["foo"].length, old.toPrettyString()) 115 ); 116 assert("a" in old["data"]["foo"]); 117 assert("b" in old["data"]["foo"]); 118 } 119 120 bool isScalar(ref const(Json) data) { 121 return data.type == Json.Type.bigInt 122 || data.type == Json.Type.bool_ 123 || data.type == Json.Type.float_ 124 || data.type == Json.Type.int_ 125 || data.type == Json.Type..string; 126 } 127 128 bool dataIsEmpty(ref const(Json) data) { 129 if(data.type == Json.Type.object) { 130 foreach(key, value; data.byKeyValue()) { 131 if(key != Constants.errors && !value.dataIsEmpty()) { 132 //if(key != Constants.errors) { // Issue #22 place to look at 133 return false; 134 } 135 } 136 return true; 137 } else if(data.type == Json.Type.null_ 138 || data.type == Json.Type.undefined 139 ) 140 { 141 return true; 142 } else if(data.type == Json.Type.array) { 143 return data.length == 0; 144 } else if(data.type == Json.Type.bigInt 145 || data.type == Json.Type.bool_ 146 || data.type == Json.Type.float_ 147 || data.type == Json.Type.int_ 148 || data.type == Json.Type..string 149 ) 150 { 151 return false; 152 } 153 154 return true; 155 } 156 157 unittest { 158 string t = `{ "errors" : {} }`; 159 Json j = parseJsonString(t); 160 assert(j.dataIsEmpty()); 161 } 162 163 unittest { 164 string t = `{ "kind": {}, "fields": null, "name": {} }`; 165 Json j = parseJsonString(t); 166 //assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22 167 assert(j.dataIsEmpty()); 168 } 169 170 unittest { 171 string t = 172 `{ 173 "name" : { 174 "foo" : null 175 } 176 }`; 177 Json j = parseJsonString(t); 178 //assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22 179 assert(j.dataIsEmpty()); 180 } 181 182 bool dataIsNull(ref const(Json) data) { 183 import std.format : format; 184 enforce(data.type == Json.Type.object, format("%s", data)); 185 if(const(Json)* d = "data" in data) { 186 return d.type == Json.Type.null_; 187 } 188 return false; 189 } 190 191 Json getWithPath(Json input, string path) { 192 auto sp = path.splitter("."); 193 foreach(s; sp) { 194 Json* n = s in input; 195 enforce(n !is null, "failed to traverse the input at " ~ s); 196 input = *n; 197 } 198 return input; 199 } 200 201 unittest { 202 string t = 203 `{ 204 "name" : { 205 "foo" : 13 206 } 207 }`; 208 Json j = parseJsonString(t); 209 Json f = j.getWithPath("name"); 210 assert("foo" in f); 211 212 f = j.getWithPath("name.foo"); 213 enforce(f.to!int() == 13); 214 215 assertThrown(j.getWithPath("doesnotexist")); 216 assertThrown(j.getWithPath("name.alsoNotThere")); 217 } 218 219 enum JoinJsonPrecedence { 220 none, 221 a, 222 b 223 } 224 225 /** Merge two Json objects. 226 Values in a take precedence over values in b. 227 */ 228 Json joinJson(JoinJsonPrecedence jjp = JoinJsonPrecedence.none)(Json a, Json b) 229 { 230 // we can not merge null or undefined values 231 if(a.type == Json.Type.null_ || a.type == Json.Type.undefined) { 232 return b; 233 } 234 if(b.type == Json.Type.null_ || b.type == Json.Type.undefined) { 235 return a; 236 } 237 238 // we need objects to merge 239 if(a.type == Json.Type.object && b.type == Json.Type.object) { 240 Json ret = a.clone(); 241 foreach(key, value; b.byKeyValue()) { 242 Json* ap = key in ret; 243 if(ap is null) { 244 ret[key] = value; 245 } else if(ap.type == Json.Type.object 246 && value.type == Json.Type.object) 247 { 248 ret[key] = joinJson(*ap, value); 249 } else { 250 static if(jjp == JoinJsonPrecedence.none) { 251 throw new Exception(format( 252 "Can not join '%s' and '%s' on key '%s'", 253 ap.type, value.type, key)); 254 } else static if(jjp == JoinJsonPrecedence.a) { 255 } else { 256 ret[key] = value; 257 } 258 } 259 } 260 return ret; 261 } 262 return a; 263 } 264 265 unittest { 266 Json a = parseJsonString(`{"overSize":200}`); 267 Json b = parseJsonString(`{}`); 268 const c = joinJson(b, a); 269 assert(c == a); 270 271 b = parseJsonString(`{"underSize":-100}`); 272 const d = joinJson(b, a); 273 immutable Json r = parseJsonString(`{"overSize":200, "underSize":-100}`); 274 assert(d == r); 275 } 276 277 unittest { 278 Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 279 parseJsonString(`{"underSize": {"b": 100}}`) 280 ); 281 282 Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`); 283 assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString())); 284 } 285 286 unittest { 287 assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 288 parseJsonString(`{"underSize": {"a": 100}}`) 289 )); 290 } 291 292 unittest { 293 assertThrown(joinJson(parseJsonString(`{"underSize": -100}`), 294 parseJsonString(`{"underSize": {"a": 100}}`) 295 )); 296 } 297 298 template toType(T) { 299 import std.bigint : BigInt; 300 import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint, 301 isSomeString; 302 static if(is(T == bool)) { 303 enum toType = Json.Type.bool_; 304 } else static if(isIntegral!(T)) { 305 enum toType = Json.Type.int_; 306 } else static if(isFloatingPoint!(T)) { 307 enum toType = Json.Type.float_; 308 } else static if(isSomeString!(T)) { 309 enum toType = Json.Type..string; 310 } else static if(isArray!(T)) { 311 enum toType = Json.Type.array; 312 } else static if(isAggregateType!(T)) { 313 enum toType = Json.Type.object; 314 } else static if(is(T == BigInt)) { 315 enum toType = Json.Type.bigint; 316 } else { 317 enum toType = Json.Type.undefined; 318 } 319 } 320 321 bool hasPathTo(T)(Json data, string path, ref T ret) { 322 enum TT = toType!T; 323 auto sp = path.splitter("."); 324 string f; 325 while(!sp.empty) { 326 f = sp.front; 327 sp.popFront(); 328 if(data.type != Json.Type.object || f !in data) { 329 return false; 330 } else { 331 data = data[f]; 332 } 333 } 334 static if(is(T == Json)) { 335 ret = data; 336 return true; 337 } else { 338 if(data.type == TT) { 339 ret = data.to!T(); 340 return true; 341 } 342 return false; 343 } 344 } 345 346 unittest { 347 Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`); 348 Json ret; 349 assert(hasPathTo!Json(d, "foo", ret)); 350 assert("path" in ret); 351 assert(ret["path"].type == Json.Type..string); 352 assert(ret["path"].get!string() == "foo"); 353 } 354 355 /** 356 params: 357 path = A "." seperated path 358 */ 359 T getWithDefault(T)(Json data, string[] paths...) { 360 enum TT = toType!T; 361 T ret = T.init; 362 foreach(string path; paths) { 363 if(hasPathTo!T(data, path, ret)) { 364 return ret; 365 } 366 } 367 return ret; 368 } 369 370 unittest { 371 Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8, 372 "__typename":"Starship","series":["DeepSpaceNine", 373 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 374 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`); 375 const r = d.getWithDefault!string("data.__typename"); 376 assert(r == "Starship", r); 377 } 378 379 unittest { 380 Json d = parseJsonString(`{"commanderId":8, 381 "__typename":"Starship","series":["DeepSpaceNine", 382 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 383 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 384 const r = d.getWithDefault!string("data.__typename", "__typename"); 385 assert(r == "Starship", r); 386 } 387 388 unittest { 389 Json d = parseJsonString(`{"commanderId":8, 390 "__typename":"Starship","series":["DeepSpaceNine", 391 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 392 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 393 const r = d.getWithDefault!string("__typename"); 394 assert(r == "Starship", r); 395 } 396 397 // TODO should return ref 398 auto accessNN(string[] tokens,T)(T tmp0) { 399 import std.array : back; 400 import std.format : format; 401 if(tmp0 !is null) { 402 static foreach(idx, token; tokens) { 403 mixin(format( 404 `if(tmp%d is null) return null; 405 auto tmp%d = tmp%d.%s;`, idx, idx+1, idx, token) 406 ); 407 } 408 return mixin(format("tmp%d", tokens.length)); 409 } 410 return null; 411 } 412 413 unittest { 414 class A { 415 int a; 416 } 417 418 class B { 419 A a; 420 } 421 422 class C { 423 B b; 424 } 425 426 auto c1 = new C; 427 assert(c1.accessNN!(["b", "a"]) is null); 428 429 c1.b = new B; 430 assert(c1.accessNN!(["b"]) !is null); 431 432 assert(c1.accessNN!(["b", "a"]) is null); 433 // TODO not sure why this is not a lvalue 434 //c1.accessNN!(["b", "a"]) = new A; 435 c1.b.a = new A; 436 assert(c1.accessNN!(["b", "a"]) !is null); 437 } 438 439 T jsonTo(T)(Json item) { 440 static import std.conv; 441 static if(is(T == enum)) { 442 enforce!GQLDExecutionException(item.type == Json.Type..string, 443 format("Enum '%s' must be passed as string not '%s'", 444 T.stringof, item.type)); 445 446 string s = item.to!string(); 447 try { 448 return std.conv.to!T(s); 449 } catch(Exception c) { 450 throw new GQLDExecutionException(c.msg); 451 } 452 } else static if(is(T == GQLDCustomLeaf!Fs, Fs...)) { 453 enforce!GQLDExecutionException(item.type == Json.Type..string, 454 format("%1$s '%1$s' must be passed as string not '%2$s'", 455 T.stringof, item.type)); 456 457 string s = item.to!string(); 458 try { 459 return T(Fs[2](s)); 460 } catch(Exception c) { 461 throw new GQLDExecutionException(c.msg); 462 } 463 } else { 464 try { 465 return item.to!T(); 466 } catch(Exception c) { 467 throw new GQLDExecutionException(c.msg); 468 } 469 } 470 } 471 472 T extract(T)(Json data, string name) { 473 enforce!GQLDExecutionException(data.type == Json.Type.object, format( 474 "Trying to get a '%s' by name '%s' but passed Json is not an object" 475 , T.stringof, name) 476 ); 477 478 Json* item = name in data; 479 480 enforce!GQLDExecutionException(item !is null, format( 481 "Trying to get a '%s' by name '%s' which is not present in passed " 482 ~ "object '%s'" 483 , T.stringof, name, data) 484 ); 485 486 return jsonTo!(T)(*item); 487 } 488 489 unittest { 490 import std.exception : assertThrown; 491 Json j = parseJsonString(`null`); 492 assertThrown(j.extract!string("Hello")); 493 } 494 495 unittest { 496 enum E { 497 no, 498 yes 499 } 500 import std.exception : assertThrown; 501 Json j = parseJsonString(`{ "foo": 1337 }`); 502 503 assertThrown(j.extract!E("foo")); 504 505 j = parseJsonString(`{ "foo": "str" }`); 506 assertThrown(j.extract!E("foo")); 507 508 j = parseJsonString(`{ "foo": "yes" }`); 509 assert(j.extract!E("foo") == E.yes); 510 } 511 512 unittest { 513 import std.exception : assertThrown; 514 Json j = parseJsonString(`{ "foo": 1337 }`); 515 immutable auto foo = j.extract!int("foo"); 516 517 assertThrown(Json.emptyObject().extract!float("Hello")); 518 assertThrown(j.extract!string("Hello")); 519 } 520 521 unittest { 522 import std.exception : assertThrown; 523 enum FooEn { 524 a, 525 b 526 } 527 Json j = parseJsonString(`{ "foo": "a" }`); 528 immutable auto foo = j.extract!FooEn("foo"); 529 assert(foo == FooEn.a); 530 531 assertThrown(Json.emptyObject().extract!float("Hello")); 532 assertThrown(j.extract!string("Hello")); 533 assert(j["foo"].jsonTo!FooEn() == FooEn.a); 534 535 Json k = parseJsonString(`{ "foo": "b" }`); 536 assert(k["foo"].jsonTo!FooEn() == FooEn.b); 537 } 538 539 const(Document) lexAndParse(string s) { 540 import graphql.lexer; 541 import graphql.parser; 542 auto l = Lexer(s, QueryParser.no); 543 auto p = Parser(l); 544 const(Document) doc = p.parseDocument(); 545 return doc; 546 } 547 548 struct StringTypeStrip { 549 string input; 550 string str; 551 bool outerNotNull; 552 bool arr; 553 bool innerNotNull; 554 555 string toString() const { 556 import std.format : format; 557 return format("StringTypeStrip(input:'%s', str:'%s', " 558 ~ "arr:'%s', outerNotNull:'%s', innerNotNull:'%s')", 559 this.input, this.str, this.arr, this.outerNotNull, 560 this.innerNotNull); 561 } 562 } 563 564 StringTypeStrip stringTypeStrip(string str) { 565 Nullable!StringTypeStrip gqld = gqldStringTypeStrip(str); 566 return gqld.get(); 567 //return gqld.isNull() 568 // ? dlangStringTypeStrip(str) 569 // : gqld.get(); 570 } 571 572 private Nullable!StringTypeStrip gqldStringTypeStrip(string str) { 573 StringTypeStrip ret; 574 ret.input = str; 575 immutable string old = str; 576 bool firstBang; 577 if(str.endsWith('!')) { 578 firstBang = true; 579 str = str[0 .. $ - 1]; 580 } 581 582 bool arr; 583 if(str.startsWith('[') && str.endsWith(']')) { 584 arr = true; 585 str = str[1 .. $ - 1]; 586 } 587 588 bool secondBang; 589 if(str.endsWith('!')) { 590 secondBang = true; 591 str = str[0 .. $ - 1]; 592 } 593 594 if(arr) { 595 ret.innerNotNull = secondBang; 596 ret.outerNotNull = firstBang; 597 } else { 598 ret.innerNotNull = firstBang; 599 } 600 601 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 602 ? "Int" 603 : str; 604 605 str = canFind(["string", "int", "float", "bool"], str) 606 ? capitalize(str) 607 : str; 608 609 str = str == "__type" ? "__Type" : str; 610 str = str == "__schema" ? "__Schema" : str; 611 str = str == "__inputvalue" ? "__InputValue" : str; 612 str = str == "__directive" ? "__Directive" : str; 613 str = str == "__field" ? "__Field" : str; 614 615 ret.arr = arr; 616 617 ret.str = str; 618 //writefln("%s %s", __LINE__, ret); 619 620 //return old == str ? Nullable!(StringTypeStrip).init : nullable(ret); 621 return nullable(ret); 622 } 623 624 unittest { 625 auto a = gqldStringTypeStrip("String"); 626 assert(!a.isNull()); 627 628 a = gqldStringTypeStrip("String!"); 629 assert(!a.isNull()); 630 assert(a.get().str == "String"); 631 assert(a.get().innerNotNull, format("%s", a.get())); 632 633 a = gqldStringTypeStrip("[String!]"); 634 assert(!a.isNull()); 635 assert(a.get().str == "String"); 636 assert(a.get().arr, format("%s", a.get())); 637 assert(a.get().innerNotNull, format("%s", a.get())); 638 639 a = gqldStringTypeStrip("[String]!"); 640 assert(!a.isNull()); 641 assert(a.get().str == "String"); 642 assert(a.get().arr, format("%s", a.get())); 643 assert(!a.get().innerNotNull, format("%s", a.get())); 644 assert(a.get().outerNotNull, format("%s", a.get())); 645 646 a = gqldStringTypeStrip("[String!]!"); 647 assert(!a.isNull()); 648 assert(a.get().str == "String"); 649 assert(a.get().arr, format("%s", a.get())); 650 assert(a.get().innerNotNull, format("%s", a.get())); 651 assert(a.get().outerNotNull, format("%s", a.get())); 652 } 653 654 private StringTypeStrip dlangStringTypeStrip(string str) { 655 StringTypeStrip ret; 656 ret.outerNotNull = true; 657 ret.innerNotNull = true; 658 ret.input = str; 659 660 immutable ns = "NullableStore!"; 661 immutable ns1 = "NullableStore!("; 662 immutable leaf = "GQLDCustomLeaf!"; 663 immutable leaf1 = "GQLDCustomLeaf!("; 664 immutable nll = "Nullable!"; 665 immutable nll1 = "Nullable!("; 666 667 // NullableStore!( .... ) 668 if(str.startsWith(ns1) && str.endsWith(")")) { 669 str = str[ns1.length .. $ - 1]; 670 } 671 672 // NullableStore!.... 673 if(str.startsWith(ns)) { 674 str = str[ns.length .. $]; 675 } 676 677 // GQLDCustomLeaf!( .... ) 678 if(str.startsWith(leaf1) && str.endsWith(")")) { 679 str = str[leaf1.length .. $ - 1]; 680 } 681 682 bool firstNull; 683 684 // Nullable!( .... ) 685 if(str.startsWith(nll1) && str.endsWith(")")) { 686 firstNull = true; 687 str = str[nll1.length .. $ - 1]; 688 } 689 690 // NullableStore!( .... ) 691 if(str.startsWith(ns1) && str.endsWith(")")) { 692 str = str[ns1.length .. $ - 1]; 693 } 694 695 // NullableStore!.... 696 if(str.startsWith(ns)) { 697 str = str[ns.length .. $]; 698 } 699 700 if(str.endsWith("!")) { 701 str = str[0 .. $ - 1]; 702 } 703 704 // xxxxxxx[] 705 if(str.endsWith("[]")) { 706 ret.arr = true; 707 str = str[0 .. $ - 2]; 708 } 709 710 bool secondNull; 711 712 // Nullable!( .... ) 713 if(str.startsWith(nll1) && str.endsWith(")")) { 714 secondNull = true; 715 str = str[nll1.length .. $ - 1]; 716 } 717 718 if(str.endsWith("!")) { 719 str = str[0 .. $ - 1]; 720 } 721 722 // Nullable! .... 723 if(str.startsWith(nll)) { 724 secondNull = true; 725 str = str[nll.length .. $]; 726 } 727 728 // NullableStore!( .... ) 729 if(str.startsWith(ns1) && str.endsWith(")")) { 730 str = str[ns1.length .. $ - 1]; 731 } 732 733 // NullableStore!.... 734 if(str.startsWith(ns)) { 735 str = str[ns.length .. $]; 736 } 737 738 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 739 ? "Int" 740 : str; 741 742 str = canFind(["string", "int", "float", "bool"], str) 743 ? capitalize(str) 744 : str; 745 746 str = str == "__type" ? "__Type" : str; 747 str = str == "__schema" ? "__Schema" : str; 748 str = str == "__inputvalue" ? "__InputValue" : str; 749 str = str == "__directive" ? "__Directive" : str; 750 str = str == "__field" ? "__Field" : str; 751 752 //writefln("firstNull %s, secondNull %s, arr %s", firstNull, secondNull, 753 // ret.arr); 754 755 if(ret.arr) { 756 ret.innerNotNull = !secondNull; 757 ret.outerNotNull = !firstNull; 758 } else { 759 ret.innerNotNull = !secondNull; 760 } 761 762 ret.str = str; 763 return ret; 764 } 765 766 unittest { 767 string t = "Nullable!string"; 768 StringTypeStrip r = t.dlangStringTypeStrip(); 769 assert(r.str == "String", to!string(r)); 770 assert(!r.arr, to!string(r)); 771 assert(!r.innerNotNull, to!string(r)); 772 assert(r.outerNotNull, to!string(r)); 773 774 t = "Nullable!(string[])"; 775 r = t.dlangStringTypeStrip(); 776 assert(r.str == "String", to!string(r)); 777 assert(r.arr, to!string(r)); 778 assert(r.innerNotNull, to!string(r)); 779 assert(!r.outerNotNull, to!string(r)); 780 } 781 782 unittest { 783 string t = "Nullable!__type"; 784 StringTypeStrip r = t.dlangStringTypeStrip(); 785 assert(r.str == "__Type", to!string(r)); 786 assert(!r.innerNotNull, to!string(r)); 787 assert(r.outerNotNull, to!string(r)); 788 assert(!r.arr, to!string(r)); 789 790 t = "Nullable!(__type[])"; 791 r = t.dlangStringTypeStrip(); 792 assert(r.str == "__Type", to!string(r)); 793 assert(r.innerNotNull, to!string(r)); 794 assert(!r.outerNotNull, to!string(r)); 795 assert(r.arr, to!string(r)); 796 } 797 798 template isClass(T) { 799 enum isClass = is(T == class); 800 } 801 802 unittest { 803 static assert(!isClass!int); 804 static assert( isClass!Object); 805 } 806 807 template isNotInTypeSet(T, R...) { 808 import std.meta : staticIndexOf; 809 enum isNotInTypeSet = staticIndexOf!(T, R) == -1; 810 } 811 812 string getTypename(Schema,T)(auto ref T input) @trusted { 813 //pragma(msg, T); 814 //writefln("To %s", T.stringof); 815 static if(!isClass!(T)) { 816 return T.stringof; 817 } else { 818 // fetch the typeinfo of the item, and compare it down until we get to a 819 // class we have. If none found, return the name of the type itself. 820 import graphql.reflection; 821 auto tinfo = typeid(input); 822 const auto reflect = SchemaReflection!Schema.instance; 823 while(tinfo !is null) { 824 if(auto cname = tinfo in reflect.classes) { 825 return *cname; 826 } 827 tinfo = tinfo.base; 828 } 829 return T.stringof; 830 } 831 } 832 833 Json toGraphqlJson(Schema,T)(auto ref T input) { 834 import std.array : empty; 835 import std.conv : to; 836 import std.typecons : Nullable; 837 import std.traits : isArray, isAggregateType, isBasicType, isSomeString, 838 isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple; 839 840 import nullablestore; 841 842 static if(isArray!T && !isSomeString!T) { 843 Json ret = Json.emptyArray(); 844 foreach(ref it; input) { 845 ret ~= toGraphqlJson!Schema(it); 846 } 847 return ret; 848 } else static if(is(T : GQLDCustomLeaf!Type, Type...)) { 849 return Json(Type[1](input)); 850 } else static if(is(T : Nullable!Type, Type)) { 851 return input.isNull() ? Json(null) : toGraphqlJson!Schema(input.get()); 852 } else static if(is(T == enum)) { 853 return Json(to!string(input)); 854 } else static if(isBasicType!T || isScalarType!T || isSomeString!T) { 855 return serializeToJson(input); 856 } else static if(isAggregateType!T) { 857 Json ret = Json.emptyObject(); 858 859 // the important bit is the setting of the __typename field 860 ret["__typename"] = getTypename!(Schema)(input); 861 //writefln("Got %s", ret["__typename"].to!string()); 862 863 alias names = FieldNameTuple!(T); 864 alias types = FieldTypeTuple!(T); 865 static foreach(idx; 0 .. names.length) {{ 866 static if(!names[idx].empty 867 && getUdaData!(T, names[idx]).ignore != Ignore.yes 868 && !is(types[idx] : NullableStore!Type, Type)) 869 { 870 static if(is(types[idx] == enum)) { 871 ret[names[idx]] = 872 to!string(__traits(getMember, input, names[idx])); 873 } else { 874 ret[names[idx]] = toGraphqlJson!Schema( 875 __traits(getMember, input, names[idx]) 876 ); 877 } 878 } 879 }} 880 return ret; 881 } else { 882 static assert(false, T.stringof ~ " not supported"); 883 } 884 } 885 886 string dtToString(DateTime dt) { 887 return dt.toISOExtString(); 888 } 889 890 DateTime stringToDT(string s) { 891 return DateTime.fromISOExtString(s); 892 } 893 894 string dToString(Date dt) { 895 return dt.toISOExtString(); 896 } 897 898 unittest { 899 import std.typecons : nullable, Nullable; 900 import nullablestore; 901 902 struct Foo { 903 int a; 904 Nullable!int b; 905 NullableStore!float c; 906 GQLDCustomLeaf!(DateTime, dtToString, stringToDT) dt2; 907 Nullable!(GQLDCustomLeaf!(DateTime, dtToString, stringToDT)) dt; 908 } 909 910 DateTime dt = DateTime(1337, 7, 1, 1, 1, 1); 911 DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3); 912 913 alias DT = GQLDCustomLeaf!(DateTime, dtToString, stringToDT); 914 915 Foo foo; 916 foo.dt2 = DT(dt2); 917 foo.dt = nullable(DT(dt)); 918 Json j = toGraphqlJson!int(foo); 919 assert(j["a"].to!int() == 0); 920 assert(j["b"].type == Json.Type.null_); 921 assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type, 922 j.toPrettyString() 923 ) 924 ); 925 immutable string exp = j["dt"].to!string(); 926 assert(exp == "1337-07-01T01:01:01", exp); 927 immutable string exp2 = j["dt2"].to!string(); 928 assert(exp2 == "2337-07-01T01:01:03", exp2); 929 930 immutable DT back = extract!DT(j, "dt"); 931 assert(back.value == dt); 932 933 immutable DT back2 = extract!DT(j, "dt2"); 934 assert(back2.value == dt2); 935 } 936 937 struct PathElement { 938 string str; 939 size_t idx; 940 941 static PathElement opCall(string s) { 942 PathElement ret; 943 ret.str = s; 944 return ret; 945 } 946 947 static PathElement opCall(size_t s) { 948 PathElement ret; 949 ret.idx = s; 950 return ret; 951 } 952 953 Json toJson() { 954 return this.str.empty ? Json(this.idx) : Json(this.str); 955 } 956 } 957 958 struct JsonCompareResult { 959 bool okay; 960 string[] path; 961 string message; 962 } 963 964 JsonCompareResult compareJson(Json a, Json b, string path 965 , bool allowArrayReorder) 966 { 967 import std.algorithm.comparison : min; 968 import std.algorithm.setops : setDifference; 969 import std.algorithm.sorting : sort; 970 import std.math : isClose; 971 972 if(a.type != b.type) { 973 return JsonCompareResult(false, [path], format("a.type %s != b.type %s" 974 , a.type, b.type)); 975 } 976 977 if(a.type == Json.Type.array) { 978 Json[] aArray = a.get!(Json[])(); 979 Json[] bArray = b.get!(Json[])(); 980 981 size_t minLength = min(aArray.length, bArray.length); 982 if(allowArrayReorder) { 983 outer: foreach(idx, it; aArray) { 984 foreach(jt; bArray) { 985 JsonCompareResult idxRslt = compareJson(it, jt 986 , format("[%s]", idx), allowArrayReorder); 987 if(idxRslt.okay) { 988 continue outer; 989 } 990 } 991 return JsonCompareResult(false, [ format("[%s]", idx) ] 992 , "No array element of 'b' matches"); 993 } 994 } else { 995 foreach(idx; 0 .. minLength) { 996 JsonCompareResult idxRslt = compareJson(aArray[idx] 997 , bArray[idx], format("[%s]", idx), allowArrayReorder); 998 if(!idxRslt.okay) { 999 return JsonCompareResult(false, [path] ~ idxRslt.path, 1000 idxRslt.message); 1001 } 1002 } 1003 } 1004 1005 if(aArray.length != bArray.length) { 1006 return JsonCompareResult(false, [path] 1007 , format("a.length %s != b.length %s", aArray.length 1008 , bArray.length)); 1009 } 1010 1011 return JsonCompareResult(true, [path], ""); 1012 } else if(a.type == Json.Type.object) { 1013 Json[string] aObj = a.get!(Json[string])(); 1014 Json[string] bObj = b.get!(Json[string])(); 1015 1016 foreach(key, value; aObj) { 1017 Json* bVal = key in bObj; 1018 if(bVal is null) { 1019 return JsonCompareResult(false, [path] 1020 , format("a[\"%s\"] not in b", key)); 1021 } else { 1022 JsonCompareResult keyRslt = compareJson(value 1023 , *bVal, format("[\"%s\"]", key), allowArrayReorder); 1024 if(!keyRslt.okay) { 1025 return JsonCompareResult(false, [path] ~ keyRslt.path, 1026 keyRslt.message); 1027 } 1028 } 1029 } 1030 auto aKeys = aObj.keys.sort; 1031 auto bKeys = bObj.keys.sort; 1032 1033 auto aMinusB = setDifference(aKeys, bKeys); 1034 auto bMinusA = setDifference(bKeys, aKeys); 1035 1036 if(!aMinusB.empty && !bMinusA.empty) { 1037 return JsonCompareResult(false, [path] 1038 , format("keys present in 'a' but not in 'b' %s, keys " 1039 ~ "present in 'b' but not in 'a' %s", aMinusB 1040 , bMinusA)); 1041 } else if(aMinusB.empty && !bMinusA.empty) { 1042 return JsonCompareResult(false, [path] 1043 , format("keys present in 'b' but not in 'a' %s", bMinusA)); 1044 } else if(!aMinusB.empty && bMinusA.empty) { 1045 return JsonCompareResult(false, [path] 1046 , format("keys present in 'a' but not in 'b' %s", aMinusB)); 1047 } 1048 return JsonCompareResult(true, [path], ""); 1049 } else if(a.type == Json.Type.Bool) { 1050 const aBool = a.get!bool(); 1051 const bBool = b.get!bool(); 1052 return JsonCompareResult(aBool == bBool, [path], format("%s != %s", aBool 1053 , bBool)); 1054 } else if(a.type == Json.Type.Int) { 1055 const aLong = a.get!long(); 1056 const bLong = b.get!long(); 1057 return JsonCompareResult(aLong == bLong, [path], format("%s != %s", aLong 1058 , bLong)); 1059 } else if(a.type == Json.Type..string) { 1060 const aStr = a.get!string(); 1061 const bStr = b.get!string(); 1062 return JsonCompareResult(aStr == bStr, [path], format("%s != %s", aStr 1063 , bStr)); 1064 } else if(a.type == Json.Type.Float) { 1065 const aFloat = a.get!double(); 1066 const bFloat = b.get!double(); 1067 return JsonCompareResult(isClose(aFloat, bFloat), [path] 1068 , format("%s != %s", aFloat, bFloat)); 1069 } 1070 return JsonCompareResult(true, [path], ""); 1071 }