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 enforce(ap.type == value.type && *ap == value 252 , format("Can not join '%s' and '%s' on key '%s'" 253 , ap.type, value.type, key) 254 ); 255 } else static if(jjp == JoinJsonPrecedence.a) { 256 } else { 257 ret[key] = value; 258 } 259 } 260 } 261 return ret; 262 } 263 return a; 264 } 265 266 unittest { 267 Json a = parseJsonString(`{"overSize":200}`); 268 Json b = parseJsonString(`{}`); 269 const c = joinJson(b, a); 270 assert(c == a); 271 272 b = parseJsonString(`{"underSize":-100}`); 273 const d = joinJson(b, a); 274 immutable Json r = parseJsonString(`{"overSize":200, "underSize":-100}`); 275 assert(d == r); 276 } 277 278 unittest { 279 Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 280 parseJsonString(`{"underSize": {"b": 100}}`) 281 ); 282 283 Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`); 284 assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString())); 285 } 286 287 unittest { 288 assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 289 parseJsonString(`{"underSize": {"a": 100}}`) 290 )); 291 } 292 293 unittest { 294 assertThrown(joinJson(parseJsonString(`{"underSize": -100}`), 295 parseJsonString(`{"underSize": {"a": 100}}`) 296 )); 297 } 298 299 template toType(T) { 300 import std.bigint : BigInt; 301 import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint, 302 isSomeString; 303 static if(is(T == bool)) { 304 enum toType = Json.Type.bool_; 305 } else static if(isIntegral!(T)) { 306 enum toType = Json.Type.int_; 307 } else static if(isFloatingPoint!(T)) { 308 enum toType = Json.Type.float_; 309 } else static if(isSomeString!(T)) { 310 enum toType = Json.Type..string; 311 } else static if(isArray!(T)) { 312 enum toType = Json.Type.array; 313 } else static if(isAggregateType!(T)) { 314 enum toType = Json.Type.object; 315 } else static if(is(T == BigInt)) { 316 enum toType = Json.Type.bigint; 317 } else { 318 enum toType = Json.Type.undefined; 319 } 320 } 321 322 bool hasPathTo(T)(Json data, string path, ref T ret) { 323 enum TT = toType!T; 324 auto sp = path.splitter("."); 325 string f; 326 while(!sp.empty) { 327 f = sp.front; 328 sp.popFront(); 329 if(data.type != Json.Type.object || f !in data) { 330 return false; 331 } else { 332 data = data[f]; 333 } 334 } 335 static if(is(T == Json)) { 336 ret = data; 337 return true; 338 } else { 339 if(data.type == TT) { 340 ret = data.to!T(); 341 return true; 342 } 343 return false; 344 } 345 } 346 347 unittest { 348 Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`); 349 Json ret; 350 assert(hasPathTo!Json(d, "foo", ret)); 351 assert("path" in ret); 352 assert(ret["path"].type == Json.Type..string); 353 assert(ret["path"].get!string() == "foo"); 354 } 355 356 /** 357 params: 358 path = A "." seperated path 359 */ 360 T getWithDefault(T)(Json data, string[] paths...) { 361 enum TT = toType!T; 362 T ret = T.init; 363 foreach(string path; paths) { 364 if(hasPathTo!T(data, path, ret)) { 365 return ret; 366 } 367 } 368 return ret; 369 } 370 371 unittest { 372 Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8, 373 "__typename":"Starship","series":["DeepSpaceNine", 374 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 375 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`); 376 const r = d.getWithDefault!string("data.__typename"); 377 assert(r == "Starship", r); 378 } 379 380 unittest { 381 Json d = parseJsonString(`{"commanderId":8, 382 "__typename":"Starship","series":["DeepSpaceNine", 383 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 384 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 385 const r = d.getWithDefault!string("data.__typename", "__typename"); 386 assert(r == "Starship", r); 387 } 388 389 unittest { 390 Json d = parseJsonString(`{"commanderId":8, 391 "__typename":"Starship","series":["DeepSpaceNine", 392 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 393 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 394 const r = d.getWithDefault!string("__typename"); 395 assert(r == "Starship", r); 396 } 397 398 // TODO should return ref 399 auto accessNN(string[] tokens,T)(T tmp0) { 400 import std.array : back; 401 import std.format : format; 402 if(tmp0 !is null) { 403 static foreach(idx, token; tokens) { 404 mixin(format( 405 `if(tmp%d is null) return null; 406 auto tmp%d = tmp%d.%s;`, idx, idx+1, idx, token) 407 ); 408 } 409 return mixin(format("tmp%d", tokens.length)); 410 } 411 return null; 412 } 413 414 unittest { 415 class A { 416 int a; 417 } 418 419 class B { 420 A a; 421 } 422 423 class C { 424 B b; 425 } 426 427 auto c1 = new C; 428 assert(c1.accessNN!(["b", "a"]) is null); 429 430 c1.b = new B; 431 assert(c1.accessNN!(["b"]) !is null); 432 433 assert(c1.accessNN!(["b", "a"]) is null); 434 // TODO not sure why this is not a lvalue 435 //c1.accessNN!(["b", "a"]) = new A; 436 c1.b.a = new A; 437 assert(c1.accessNN!(["b", "a"]) !is null); 438 } 439 440 T jsonTo(T)(Json item) { 441 static import std.conv; 442 static if(is(T == enum)) { 443 enforce!GQLDExecutionException(item.type == Json.Type..string, 444 format("Enum '%s' must be passed as string not '%s'", 445 T.stringof, item.type)); 446 447 string s = item.to!string(); 448 try { 449 return std.conv.to!T(s); 450 } catch(Exception c) { 451 throw new GQLDExecutionException(c.msg); 452 } 453 } else static if(is(T == GQLDCustomLeaf!Fs, Fs...)) { 454 enforce!GQLDExecutionException(item.type == Json.Type..string, 455 format("%1$s '%1$s' must be passed as string not '%2$s'", 456 T.stringof, item.type)); 457 458 string s = item.to!string(); 459 try { 460 return T(Fs[2](s)); 461 } catch(Exception c) { 462 throw new GQLDExecutionException(c.msg); 463 } 464 } else { 465 try { 466 return item.to!T(); 467 } catch(Exception c) { 468 throw new GQLDExecutionException(c.msg); 469 } 470 } 471 } 472 473 T extract(T)(Json data, string name) { 474 enforce!GQLDExecutionException(data.type == Json.Type.object, format( 475 "Trying to get a '%s' by name '%s' but passed Json is not an object" 476 , T.stringof, name) 477 ); 478 479 Json* item = name in data; 480 481 enforce!GQLDExecutionException(item !is null, format( 482 "Trying to get a '%s' by name '%s' which is not present in passed " 483 ~ "object '%s'" 484 , T.stringof, name, data) 485 ); 486 487 return jsonTo!(T)(*item); 488 } 489 490 unittest { 491 import std.exception : assertThrown; 492 Json j = parseJsonString(`null`); 493 assertThrown(j.extract!string("Hello")); 494 } 495 496 unittest { 497 enum E { 498 no, 499 yes 500 } 501 import std.exception : assertThrown; 502 Json j = parseJsonString(`{ "foo": 1337 }`); 503 504 assertThrown(j.extract!E("foo")); 505 506 j = parseJsonString(`{ "foo": "str" }`); 507 assertThrown(j.extract!E("foo")); 508 509 j = parseJsonString(`{ "foo": "yes" }`); 510 assert(j.extract!E("foo") == E.yes); 511 } 512 513 unittest { 514 import std.exception : assertThrown; 515 Json j = parseJsonString(`{ "foo": 1337 }`); 516 immutable auto foo = j.extract!int("foo"); 517 518 assertThrown(Json.emptyObject().extract!float("Hello")); 519 assertThrown(j.extract!string("Hello")); 520 } 521 522 unittest { 523 import std.exception : assertThrown; 524 enum FooEn { 525 a, 526 b 527 } 528 Json j = parseJsonString(`{ "foo": "a" }`); 529 immutable auto foo = j.extract!FooEn("foo"); 530 assert(foo == FooEn.a); 531 532 assertThrown(Json.emptyObject().extract!float("Hello")); 533 assertThrown(j.extract!string("Hello")); 534 assert(j["foo"].jsonTo!FooEn() == FooEn.a); 535 536 Json k = parseJsonString(`{ "foo": "b" }`); 537 assert(k["foo"].jsonTo!FooEn() == FooEn.b); 538 } 539 540 const(Document) lexAndParse(string s) { 541 import graphql.lexer; 542 import graphql.parser; 543 auto l = Lexer(s, QueryParser.no); 544 auto p = Parser(l); 545 const(Document) doc = p.parseDocument(); 546 return doc; 547 } 548 549 struct StringTypeStrip { 550 string input; 551 string str; 552 bool outerNotNull; 553 bool arr; 554 bool innerNotNull; 555 556 string toString() const { 557 import std.format : format; 558 return format("StringTypeStrip(input:'%s', str:'%s', " 559 ~ "arr:'%s', outerNotNull:'%s', innerNotNull:'%s')", 560 this.input, this.str, this.arr, this.outerNotNull, 561 this.innerNotNull); 562 } 563 } 564 565 StringTypeStrip stringTypeStrip(string str) { 566 Nullable!StringTypeStrip gqld = gqldStringTypeStrip(str); 567 return gqld.get(); 568 //return gqld.isNull() 569 // ? dlangStringTypeStrip(str) 570 // : gqld.get(); 571 } 572 573 private Nullable!StringTypeStrip gqldStringTypeStrip(string str) { 574 StringTypeStrip ret; 575 ret.input = str; 576 immutable string old = str; 577 bool firstBang; 578 if(str.endsWith('!')) { 579 firstBang = true; 580 str = str[0 .. $ - 1]; 581 } 582 583 bool arr; 584 if(str.startsWith('[') && str.endsWith(']')) { 585 arr = true; 586 str = str[1 .. $ - 1]; 587 } 588 589 bool secondBang; 590 if(str.endsWith('!')) { 591 secondBang = true; 592 str = str[0 .. $ - 1]; 593 } 594 595 if(arr) { 596 ret.innerNotNull = secondBang; 597 ret.outerNotNull = firstBang; 598 } else { 599 ret.innerNotNull = firstBang; 600 } 601 602 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 603 ? "Int" 604 : str; 605 606 str = canFind(["string", "int", "float", "bool"], str) 607 ? capitalize(str) 608 : str; 609 610 str = str == "__type" ? "__Type" : str; 611 str = str == "__schema" ? "__Schema" : str; 612 str = str == "__inputvalue" ? "__InputValue" : str; 613 str = str == "__directive" ? "__Directive" : str; 614 str = str == "__field" ? "__Field" : str; 615 616 ret.arr = arr; 617 618 ret.str = str; 619 //writefln("%s %s", __LINE__, ret); 620 621 //return old == str ? Nullable!(StringTypeStrip).init : nullable(ret); 622 return nullable(ret); 623 } 624 625 unittest { 626 auto a = gqldStringTypeStrip("String"); 627 assert(!a.isNull()); 628 629 a = gqldStringTypeStrip("String!"); 630 assert(!a.isNull()); 631 assert(a.get().str == "String"); 632 assert(a.get().innerNotNull, format("%s", a.get())); 633 634 a = gqldStringTypeStrip("[String!]"); 635 assert(!a.isNull()); 636 assert(a.get().str == "String"); 637 assert(a.get().arr, format("%s", a.get())); 638 assert(a.get().innerNotNull, format("%s", a.get())); 639 640 a = gqldStringTypeStrip("[String]!"); 641 assert(!a.isNull()); 642 assert(a.get().str == "String"); 643 assert(a.get().arr, format("%s", a.get())); 644 assert(!a.get().innerNotNull, format("%s", a.get())); 645 assert(a.get().outerNotNull, format("%s", a.get())); 646 647 a = gqldStringTypeStrip("[String!]!"); 648 assert(!a.isNull()); 649 assert(a.get().str == "String"); 650 assert(a.get().arr, format("%s", a.get())); 651 assert(a.get().innerNotNull, format("%s", a.get())); 652 assert(a.get().outerNotNull, format("%s", a.get())); 653 } 654 655 private StringTypeStrip dlangStringTypeStrip(string str) { 656 StringTypeStrip ret; 657 ret.outerNotNull = true; 658 ret.innerNotNull = true; 659 ret.input = str; 660 661 immutable ns = "NullableStore!"; 662 immutable ns1 = "NullableStore!("; 663 immutable leaf = "GQLDCustomLeaf!"; 664 immutable leaf1 = "GQLDCustomLeaf!("; 665 immutable nll = "Nullable!"; 666 immutable nll1 = "Nullable!("; 667 668 // NullableStore!( .... ) 669 if(str.startsWith(ns1) && str.endsWith(")")) { 670 str = str[ns1.length .. $ - 1]; 671 } 672 673 // NullableStore!.... 674 if(str.startsWith(ns)) { 675 str = str[ns.length .. $]; 676 } 677 678 // GQLDCustomLeaf!( .... ) 679 if(str.startsWith(leaf1) && str.endsWith(")")) { 680 str = str[leaf1.length .. $ - 1]; 681 } 682 683 bool firstNull; 684 685 // Nullable!( .... ) 686 if(str.startsWith(nll1) && str.endsWith(")")) { 687 firstNull = true; 688 str = str[nll1.length .. $ - 1]; 689 } 690 691 // NullableStore!( .... ) 692 if(str.startsWith(ns1) && str.endsWith(")")) { 693 str = str[ns1.length .. $ - 1]; 694 } 695 696 // NullableStore!.... 697 if(str.startsWith(ns)) { 698 str = str[ns.length .. $]; 699 } 700 701 if(str.endsWith("!")) { 702 str = str[0 .. $ - 1]; 703 } 704 705 // xxxxxxx[] 706 if(str.endsWith("[]")) { 707 ret.arr = true; 708 str = str[0 .. $ - 2]; 709 } 710 711 bool secondNull; 712 713 // Nullable!( .... ) 714 if(str.startsWith(nll1) && str.endsWith(")")) { 715 secondNull = true; 716 str = str[nll1.length .. $ - 1]; 717 } 718 719 if(str.endsWith("!")) { 720 str = str[0 .. $ - 1]; 721 } 722 723 // Nullable! .... 724 if(str.startsWith(nll)) { 725 secondNull = true; 726 str = str[nll.length .. $]; 727 } 728 729 // NullableStore!( .... ) 730 if(str.startsWith(ns1) && str.endsWith(")")) { 731 str = str[ns1.length .. $ - 1]; 732 } 733 734 // NullableStore!.... 735 if(str.startsWith(ns)) { 736 str = str[ns.length .. $]; 737 } 738 739 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 740 ? "Int" 741 : str; 742 743 str = canFind(["string", "int", "float", "bool"], str) 744 ? capitalize(str) 745 : str; 746 747 str = str == "__type" ? "__Type" : str; 748 str = str == "__schema" ? "__Schema" : str; 749 str = str == "__inputvalue" ? "__InputValue" : str; 750 str = str == "__directive" ? "__Directive" : str; 751 str = str == "__field" ? "__Field" : str; 752 753 //writefln("firstNull %s, secondNull %s, arr %s", firstNull, secondNull, 754 // ret.arr); 755 756 if(ret.arr) { 757 ret.innerNotNull = !secondNull; 758 ret.outerNotNull = !firstNull; 759 } else { 760 ret.innerNotNull = !secondNull; 761 } 762 763 ret.str = str; 764 return ret; 765 } 766 767 unittest { 768 string t = "Nullable!string"; 769 StringTypeStrip r = t.dlangStringTypeStrip(); 770 assert(r.str == "String", to!string(r)); 771 assert(!r.arr, to!string(r)); 772 assert(!r.innerNotNull, to!string(r)); 773 assert(r.outerNotNull, to!string(r)); 774 775 t = "Nullable!(string[])"; 776 r = t.dlangStringTypeStrip(); 777 assert(r.str == "String", to!string(r)); 778 assert(r.arr, to!string(r)); 779 assert(r.innerNotNull, to!string(r)); 780 assert(!r.outerNotNull, to!string(r)); 781 } 782 783 unittest { 784 string t = "Nullable!__type"; 785 StringTypeStrip r = t.dlangStringTypeStrip(); 786 assert(r.str == "__Type", to!string(r)); 787 assert(!r.innerNotNull, to!string(r)); 788 assert(r.outerNotNull, to!string(r)); 789 assert(!r.arr, to!string(r)); 790 791 t = "Nullable!(__type[])"; 792 r = t.dlangStringTypeStrip(); 793 assert(r.str == "__Type", to!string(r)); 794 assert(r.innerNotNull, to!string(r)); 795 assert(!r.outerNotNull, to!string(r)); 796 assert(r.arr, to!string(r)); 797 } 798 799 template isClass(T) { 800 enum isClass = is(T == class); 801 } 802 803 unittest { 804 static assert(!isClass!int); 805 static assert( isClass!Object); 806 } 807 808 template isNotInTypeSet(T, R...) { 809 import std.meta : staticIndexOf; 810 enum isNotInTypeSet = staticIndexOf!(T, R) == -1; 811 } 812 813 string getTypename(Schema,T)(auto ref T input) @trusted { 814 //pragma(msg, T); 815 //writefln("To %s", T.stringof); 816 static if(!isClass!(T)) { 817 return T.stringof; 818 } else { 819 // fetch the typeinfo of the item, and compare it down until we get to a 820 // class we have. If none found, return the name of the type itself. 821 import graphql.reflection; 822 auto tinfo = typeid(input); 823 const auto reflect = SchemaReflection!Schema.instance; 824 while(tinfo !is null) { 825 if(auto cname = tinfo in reflect.classes) { 826 return *cname; 827 } 828 tinfo = tinfo.base; 829 } 830 return T.stringof; 831 } 832 } 833 834 Json toGraphqlJson(Schema,T)(auto ref T input) { 835 import std.array : empty; 836 import std.conv : to; 837 import std.typecons : Nullable; 838 import std.traits : isArray, isAggregateType, isBasicType, isSomeString, 839 isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple; 840 841 import nullablestore; 842 843 static if(isArray!T && !isSomeString!T) { 844 Json ret = Json.emptyArray(); 845 foreach(ref it; input) { 846 ret ~= toGraphqlJson!Schema(it); 847 } 848 return ret; 849 } else static if(is(T : GQLDCustomLeaf!Type, Type...)) { 850 return Json(Type[1](input)); 851 } else static if(is(T : Nullable!Type, Type)) { 852 return input.isNull() ? Json(null) : toGraphqlJson!Schema(input.get()); 853 } else static if(is(T == enum)) { 854 return Json(to!string(input)); 855 } else static if(isBasicType!T || isScalarType!T || isSomeString!T) { 856 return serializeToJson(input); 857 } else static if(isAggregateType!T) { 858 Json ret = Json.emptyObject(); 859 860 // the important bit is the setting of the __typename field 861 ret["__typename"] = getTypename!(Schema)(input); 862 //writefln("Got %s", ret["__typename"].to!string()); 863 864 alias names = FieldNameTuple!(T); 865 alias types = FieldTypeTuple!(T); 866 static foreach(idx; 0 .. names.length) {{ 867 static if(!names[idx].empty 868 && getUdaData!(T, names[idx]).ignore != Ignore.yes 869 && !is(types[idx] : NullableStore!Type, Type)) 870 { 871 static if(is(types[idx] == enum)) { 872 ret[names[idx]] = 873 to!string(__traits(getMember, input, names[idx])); 874 } else { 875 ret[names[idx]] = toGraphqlJson!Schema( 876 __traits(getMember, input, names[idx]) 877 ); 878 } 879 } 880 }} 881 return ret; 882 } else { 883 static assert(false, T.stringof ~ " not supported"); 884 } 885 } 886 887 string dtToString(DateTime dt) { 888 return dt.toISOExtString(); 889 } 890 891 DateTime stringToDT(string s) { 892 return DateTime.fromISOExtString(s); 893 } 894 895 string dToString(Date dt) { 896 return dt.toISOExtString(); 897 } 898 899 unittest { 900 import std.typecons : nullable, Nullable; 901 import nullablestore; 902 903 struct Foo { 904 int a; 905 Nullable!int b; 906 NullableStore!float c; 907 GQLDCustomLeaf!(DateTime, dtToString, stringToDT) dt2; 908 Nullable!(GQLDCustomLeaf!(DateTime, dtToString, stringToDT)) dt; 909 } 910 911 DateTime dt = DateTime(1337, 7, 1, 1, 1, 1); 912 DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3); 913 914 alias DT = GQLDCustomLeaf!(DateTime, dtToString, stringToDT); 915 916 Foo foo; 917 foo.dt2 = DT(dt2); 918 foo.dt = nullable(DT(dt)); 919 Json j = toGraphqlJson!int(foo); 920 assert(j["a"].to!int() == 0); 921 assert(j["b"].type == Json.Type.null_); 922 assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type, 923 j.toPrettyString() 924 ) 925 ); 926 immutable string exp = j["dt"].to!string(); 927 assert(exp == "1337-07-01T01:01:01", exp); 928 immutable string exp2 = j["dt2"].to!string(); 929 assert(exp2 == "2337-07-01T01:01:03", exp2); 930 931 immutable DT back = extract!DT(j, "dt"); 932 assert(back.value == dt); 933 934 immutable DT back2 = extract!DT(j, "dt2"); 935 assert(back2.value == dt2); 936 } 937 938 struct PathElement { 939 string str; 940 size_t idx; 941 942 static PathElement opCall(string s) { 943 PathElement ret; 944 ret.str = s; 945 return ret; 946 } 947 948 static PathElement opCall(size_t s) { 949 PathElement ret; 950 ret.idx = s; 951 return ret; 952 } 953 954 Json toJson() { 955 return this.str.empty ? Json(this.idx) : Json(this.str); 956 } 957 } 958 959 struct JsonCompareResult { 960 bool okay; 961 string[] path; 962 string message; 963 } 964 965 JsonCompareResult compareJson(Json a, Json b, string path 966 , bool allowArrayReorder) 967 { 968 import std.algorithm.comparison : min; 969 import std.algorithm.setops : setDifference; 970 import std.algorithm.sorting : sort; 971 import std.math : isClose; 972 973 if(a.type != b.type) { 974 return JsonCompareResult(false, [path], format("a.type %s != b.type %s" 975 , a.type, b.type)); 976 } 977 978 if(a.type == Json.Type.array) { 979 Json[] aArray = a.get!(Json[])(); 980 Json[] bArray = b.get!(Json[])(); 981 982 size_t minLength = min(aArray.length, bArray.length); 983 if(allowArrayReorder) { 984 outer: foreach(idx, it; aArray) { 985 foreach(jt; bArray) { 986 JsonCompareResult idxRslt = compareJson(it, jt 987 , format("[%s]", idx), allowArrayReorder); 988 if(idxRslt.okay) { 989 continue outer; 990 } 991 } 992 return JsonCompareResult(false, [ format("[%s]", idx) ] 993 , "No array element of 'b' matches"); 994 } 995 } else { 996 foreach(idx; 0 .. minLength) { 997 JsonCompareResult idxRslt = compareJson(aArray[idx] 998 , bArray[idx], format("[%s]", idx), allowArrayReorder); 999 if(!idxRslt.okay) { 1000 return JsonCompareResult(false, [path] ~ idxRslt.path, 1001 idxRslt.message); 1002 } 1003 } 1004 } 1005 1006 if(aArray.length != bArray.length) { 1007 return JsonCompareResult(false, [path] 1008 , format("a.length %s != b.length %s", aArray.length 1009 , bArray.length)); 1010 } 1011 1012 return JsonCompareResult(true, [path], ""); 1013 } else if(a.type == Json.Type.object) { 1014 Json[string] aObj = a.get!(Json[string])(); 1015 Json[string] bObj = b.get!(Json[string])(); 1016 1017 foreach(key, value; aObj) { 1018 Json* bVal = key in bObj; 1019 if(bVal is null) { 1020 return JsonCompareResult(false, [path] 1021 , format("a[\"%s\"] not in b", key)); 1022 } else { 1023 JsonCompareResult keyRslt = compareJson(value 1024 , *bVal, format("[\"%s\"]", key), allowArrayReorder); 1025 if(!keyRslt.okay) { 1026 return JsonCompareResult(false, [path] ~ keyRslt.path, 1027 keyRslt.message); 1028 } 1029 } 1030 } 1031 auto aKeys = aObj.keys.sort; 1032 auto bKeys = bObj.keys.sort; 1033 1034 auto aMinusB = setDifference(aKeys, bKeys); 1035 auto bMinusA = setDifference(bKeys, aKeys); 1036 1037 if(!aMinusB.empty && !bMinusA.empty) { 1038 return JsonCompareResult(false, [path] 1039 , format("keys present in 'a' but not in 'b' %s, keys " 1040 ~ "present in 'b' but not in 'a' %s", aMinusB 1041 , bMinusA)); 1042 } else if(aMinusB.empty && !bMinusA.empty) { 1043 return JsonCompareResult(false, [path] 1044 , format("keys present in 'b' but not in 'a' %s", bMinusA)); 1045 } else if(!aMinusB.empty && bMinusA.empty) { 1046 return JsonCompareResult(false, [path] 1047 , format("keys present in 'a' but not in 'b' %s", aMinusB)); 1048 } 1049 return JsonCompareResult(true, [path], ""); 1050 } else if(a.type == Json.Type.Bool) { 1051 const aBool = a.get!bool(); 1052 const bBool = b.get!bool(); 1053 return JsonCompareResult(aBool == bBool, [path], format("%s != %s", aBool 1054 , bBool)); 1055 } else if(a.type == Json.Type.Int) { 1056 const aLong = a.get!long(); 1057 const bLong = b.get!long(); 1058 return JsonCompareResult(aLong == bLong, [path], format("%s != %s", aLong 1059 , bLong)); 1060 } else if(a.type == Json.Type..string) { 1061 const aStr = a.get!string(); 1062 const bStr = b.get!string(); 1063 return JsonCompareResult(aStr == bStr, [path], format("%s != %s", aStr 1064 , bStr)); 1065 } else if(a.type == Json.Type.Float) { 1066 const aFloat = a.get!double(); 1067 const bFloat = b.get!double(); 1068 return JsonCompareResult(isClose(aFloat, bFloat), [path] 1069 , format("%s != %s", aFloat, bFloat)); 1070 } 1071 return JsonCompareResult(true, [path], ""); 1072 }