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.experimental.logger; 10 import std.format : format; 11 import std.stdio; 12 import std.string : capitalize, indexOf, strip; 13 import std.typecons : nullable, Nullable; 14 15 import vibe.data.json; 16 17 import graphql.ast; 18 import graphql.constants; 19 20 @safe: 21 22 enum d = "data"; 23 enum e = Constants.errors; 24 25 string firstCharUpperCase(string input) { 26 import std.conv : to; 27 import std.uni : isUpper, toUpper; 28 import std.array : front, popFront; 29 if(isUpper(input.front)) { 30 return input; 31 } 32 33 const f = input.front; 34 input.popFront(); 35 36 return to!string(toUpper(f)) ~ input; 37 } 38 39 Json returnTemplate() { 40 Json ret = Json.emptyObject(); 41 ret["data"] = Json.emptyObject(); 42 ret[Constants.errors] = Json.emptyArray(); 43 return ret; 44 } 45 46 void insertError(T)(ref Json result, T t) { 47 insertError(result, t, []); 48 } 49 50 void insertError(T)(ref Json result, T t, PathElement[] path) { 51 Json tmp = Json.emptyObject(); 52 tmp["message"] = serializeToJson(t); 53 if(!path.empty) { 54 tmp["path"] = Json.emptyArray(); 55 path.each!(it => tmp["path"] ~= it.toJson()); 56 } 57 if(e !in result) { 58 result[e] = Json.emptyArray(); 59 } 60 enforce(result[e].type == Json.Type.array); 61 result[e] ~= tmp; 62 } 63 64 void insertPayload(ref Json result, string field, Json data) { 65 if(d in data) { 66 if(d !in result) { 67 result[d] = Json.emptyObject(); 68 } 69 enforce(result[d].type == Json.Type.object); 70 Json* df = field in result[d]; 71 if(df) { 72 result[d][field] = joinJson(*df, data[d]); 73 } else { 74 result[d][field] = data[d]; 75 } 76 } 77 if(e in data) { 78 if(e !in result) { 79 result[e] = Json.emptyArray(); 80 } 81 enforce(result[e].type == Json.Type.array); 82 if(!canFind(result[e].byValue(), data[e])) { 83 result[e] ~= data[e]; 84 } 85 } 86 } 87 88 unittest { 89 Json old = returnTemplate(); 90 old["data"]["foo"] = Json.emptyObject(); 91 old["data"]["foo"]["a"] = 1337; 92 93 Json n = returnTemplate(); 94 n["data"] = Json.emptyObject(); 95 n["data"]["b"] = 1338; 96 97 old.insertPayload("foo", n); 98 assert(old["data"]["foo"].length == 2, format("%s %s", 99 old["data"]["foo"].length, old.toPrettyString()) 100 ); 101 assert("a" in old["data"]["foo"]); 102 assert("b" in old["data"]["foo"]); 103 } 104 105 bool isScalar(ref const(Json) data) { 106 return data.type == Json.Type.bigInt 107 || data.type == Json.Type.bool_ 108 || data.type == Json.Type.float_ 109 || data.type == Json.Type.int_ 110 || data.type == Json.Type..string; 111 } 112 113 bool dataIsEmpty(ref const(Json) data) { 114 import std.experimental.logger; 115 if(data.type == Json.Type.object) { 116 foreach(key, value; data.byKeyValue()) { 117 if(key != Constants.errors && !value.dataIsEmpty()) { 118 //if(key != Constants.errors) { // Issue #22 place to look at 119 return false; 120 } 121 } 122 return true; 123 } else if(data.type == Json.Type.null_ 124 || data.type == Json.Type.undefined 125 ) 126 { 127 return true; 128 } else if(data.type == Json.Type.array) { 129 return data.length == 0; 130 } else if(data.type == Json.Type.bigInt 131 || data.type == Json.Type.bool_ 132 || data.type == Json.Type.float_ 133 || data.type == Json.Type.int_ 134 || data.type == Json.Type..string 135 ) 136 { 137 return false; 138 } 139 140 return true; 141 } 142 143 unittest { 144 string t = `{ "errors" : {} }`; 145 Json j = parseJsonString(t); 146 assert(j.dataIsEmpty()); 147 } 148 149 unittest { 150 string t = `{ "kind": {}, "fields": null, "name": {} }`; 151 Json j = parseJsonString(t); 152 //assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22 153 assert(j.dataIsEmpty()); 154 } 155 156 unittest { 157 string t = 158 `{ 159 "name" : { 160 "foo" : null 161 } 162 }`; 163 Json j = parseJsonString(t); 164 //assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22 165 assert(j.dataIsEmpty()); 166 } 167 168 bool dataIsNull(ref const(Json) data) { 169 import std.format : format; 170 enforce(data.type == Json.Type.object, format("%s", data)); 171 if(const(Json)* d = "data" in data) { 172 return d.type == Json.Type.null_; 173 } 174 return false; 175 } 176 177 Json getWithPath(Json input, string path) { 178 auto sp = path.splitter("."); 179 foreach(s; sp) { 180 Json* n = s in input; 181 enforce(n !is null, "failed to traverse the input at " ~ s); 182 input = *n; 183 } 184 return input; 185 } 186 187 unittest { 188 string t = 189 `{ 190 "name" : { 191 "foo" : 13 192 } 193 }`; 194 Json j = parseJsonString(t); 195 Json f = j.getWithPath("name"); 196 assert("foo" in f); 197 198 f = j.getWithPath("name.foo"); 199 enforce(f.to!int() == 13); 200 201 assertThrown(j.getWithPath("doesnotexist")); 202 assertThrown(j.getWithPath("name.alsoNotThere")); 203 } 204 205 enum JoinJsonPrecedence { 206 none, 207 a, 208 b 209 } 210 211 /** Merge two Json objects. 212 Values in a take precedence over values in b. 213 */ 214 Json joinJson(JoinJsonPrecedence jjp = JoinJsonPrecedence.none)(Json a, Json b) 215 { 216 // we can not merge null or undefined values 217 if(a.type == Json.Type.null_ || a.type == Json.Type.undefined) { 218 return b; 219 } 220 if(b.type == Json.Type.null_ || b.type == Json.Type.undefined) { 221 return a; 222 } 223 224 // we need objects to merge 225 if(a.type == Json.Type.object && b.type == Json.Type.object) { 226 Json ret = a.clone(); 227 foreach(key, value; b.byKeyValue()) { 228 Json* ap = key in ret; 229 if(ap is null) { 230 ret[key] = value; 231 } else if(ap.type == Json.Type.object 232 && value.type == Json.Type.object) 233 { 234 ret[key] = joinJson(*ap, value); 235 } else { 236 static if(jjp == JoinJsonPrecedence.none) { 237 throw new Exception(format( 238 "Can not join '%s' and '%s' on key '%s'", 239 ap.type, value.type, key)); 240 } else static if(jjp == JoinJsonPrecedence.a) { 241 } else { 242 ret[key] = value; 243 } 244 } 245 } 246 return ret; 247 } 248 return a; 249 } 250 251 unittest { 252 Json a = parseJsonString(`{"overSize":200}`); 253 Json b = parseJsonString(`{}`); 254 const c = joinJson(b, a); 255 assert(c == a); 256 257 b = parseJsonString(`{"underSize":-100}`); 258 const d = joinJson(b, a); 259 Json r = parseJsonString(`{"overSize":200, "underSize":-100}`); 260 assert(d == r); 261 } 262 263 unittest { 264 Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 265 parseJsonString(`{"underSize": {"b": 100}}`) 266 ); 267 268 Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`); 269 assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString())); 270 } 271 272 unittest { 273 assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`), 274 parseJsonString(`{"underSize": {"a": 100}}`) 275 )); 276 } 277 278 unittest { 279 assertThrown(joinJson(parseJsonString(`{"underSize": -100}`), 280 parseJsonString(`{"underSize": {"a": 100}}`) 281 )); 282 } 283 284 template toType(T) { 285 import std.bigint : BigInt; 286 import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint, 287 isSomeString; 288 static if(is(T == bool)) { 289 enum toType = Json.Type.bool_; 290 } else static if(isIntegral!(T)) { 291 enum toType = Json.Type.int_; 292 } else static if(isFloatingPoint!(T)) { 293 enum toType = Json.Type.float_; 294 } else static if(isSomeString!(T)) { 295 enum toType = Json.Type..string; 296 } else static if(isArray!(T)) { 297 enum toType = Json.Type.array; 298 } else static if(isAggregateType!(T)) { 299 enum toType = Json.Type.object; 300 } else static if(is(T == BigInt)) { 301 enum toType = Json.Type.bigint; 302 } else { 303 enum toType = Json.Type.undefined; 304 } 305 } 306 307 bool hasPathTo(T)(Json data, string path, ref T ret) { 308 enum TT = toType!T; 309 auto sp = path.splitter("."); 310 string f; 311 while(!sp.empty) { 312 f = sp.front; 313 sp.popFront(); 314 if(data.type != Json.Type.object || f !in data) { 315 return false; 316 } else { 317 data = data[f]; 318 } 319 } 320 static if(is(T == Json)) { 321 ret = data; 322 return true; 323 } else { 324 if(data.type == TT) { 325 ret = data.to!T(); 326 return true; 327 } 328 return false; 329 } 330 } 331 332 unittest { 333 Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`); 334 Json ret; 335 assert(hasPathTo!Json(d, "foo", ret)); 336 } 337 338 /** 339 params: 340 path = A "." seperated path 341 */ 342 T getWithDefault(T)(Json data, string[] paths...) { 343 enum TT = toType!T; 344 T ret = T.init; 345 foreach(string path; paths) { 346 if(hasPathTo!T(data, path, ret)) { 347 return ret; 348 } 349 } 350 return ret; 351 } 352 353 unittest { 354 Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8, 355 "__typename":"Starship","series":["DeepSpaceNine", 356 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 357 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`); 358 const r = d.getWithDefault!string("data.__typename"); 359 assert(r == "Starship", r); 360 } 361 362 unittest { 363 Json d = parseJsonString(`{"commanderId":8, 364 "__typename":"Starship","series":["DeepSpaceNine", 365 "TheOriginalSeries"],"id":43,"name":"Defiant","size":130, 366 "crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`); 367 const r = d.getWithDefault!string("data.__typename", "__typename"); 368 assert(r == "Starship", r); 369 } 370 371 unittest { 372 Json d = parseJsonString(`{"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("__typename"); 377 assert(r == "Starship", r); 378 } 379 380 // TODO should return ref 381 auto accessNN(string[] tokens,T)(T tmp0) { 382 import std.array : back; 383 import std.format : format; 384 if(tmp0 !is null) { 385 static foreach(idx, token; tokens) { 386 mixin(format! 387 `if(tmp%d is null) return null; 388 auto tmp%d = tmp%d.%s;`(idx, idx+1, idx, token) 389 ); 390 } 391 return mixin(format("tmp%d", tokens.length)); 392 } 393 return null; 394 } 395 396 unittest { 397 class A { 398 int a; 399 } 400 401 class B { 402 A a; 403 } 404 405 class C { 406 B b; 407 } 408 409 auto c1 = new C; 410 assert(c1.accessNN!(["b", "a"]) is null); 411 412 c1.b = new B; 413 assert(c1.accessNN!(["b"]) !is null); 414 415 assert(c1.accessNN!(["b", "a"]) is null); 416 // TODO not sure why this is not a lvalue 417 //c1.accessNN!(["b", "a"]) = new A; 418 c1.b.a = new A; 419 assert(c1.accessNN!(["b", "a"]) !is null); 420 } 421 422 T extract(T)(Json data, string name) { 423 enforce(data.type == Json.Type.object, format! 424 "Trying to get a '%s' by name '%s' but passed Json is not an object" 425 (T.stringof, name) 426 ); 427 428 Json* item = name in data; 429 430 enforce(item !is null, format!( 431 "Trying to get a '%s' by name '%s' which is not present in passed " 432 ~ "object '%s'" 433 )(T.stringof, name, data) 434 ); 435 436 return (*item).to!T(); 437 } 438 439 unittest { 440 import std.exception : assertThrown; 441 Json j = parseJsonString(`{ "foo": 1337 }`); 442 auto foo = j.extract!int("foo"); 443 444 assertThrown(Json.emptyObject().extract!float("Hello")); 445 assertThrown(j.extract!string("Hello")); 446 } 447 448 const(Document) lexAndParse(string s) { 449 import graphql.lexer; 450 import graphql.parser; 451 auto l = Lexer(s, QueryParser.no); 452 auto p = Parser(l); 453 const(Document) doc = p.parseDocument(); 454 return doc; 455 } 456 457 struct StringTypeStrip { 458 string input; 459 string str; 460 bool outerNotNull; 461 bool arr; 462 bool innerNotNull; 463 464 string toString() const { 465 import std.format : format; 466 return format("StringTypeStrip(input:'%s', str:'%s', " 467 ~ "arr:'%s', outerNotNull:'%s', innerNotNull:'%s')", 468 this.input, this.str, this.arr, this.outerNotNull, 469 this.innerNotNull); 470 } 471 } 472 473 StringTypeStrip stringTypeStrip(string str) { 474 Nullable!StringTypeStrip gqld = gqldStringTypeStrip(str); 475 return gqld.get(); 476 //return gqld.isNull() 477 // ? dlangStringTypeStrip(str) 478 // : gqld.get(); 479 } 480 481 private Nullable!StringTypeStrip gqldStringTypeStrip(string str) { 482 StringTypeStrip ret; 483 ret.input = str; 484 string old = str; 485 bool firstBang; 486 if(str.endsWith('!')) { 487 firstBang = true; 488 str = str[0 .. $ - 1]; 489 } 490 491 bool arr; 492 if(str.startsWith('[') && str.endsWith(']')) { 493 arr = true; 494 str = str[1 .. $ - 1]; 495 } 496 497 bool secondBang; 498 if(str.endsWith('!')) { 499 secondBang = true; 500 str = str[0 .. $ - 1]; 501 } 502 503 if(arr) { 504 ret.innerNotNull = secondBang; 505 ret.outerNotNull = firstBang; 506 } else { 507 ret.innerNotNull = firstBang; 508 } 509 510 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 511 ? "Int" 512 : str; 513 514 str = canFind(["string", "int", "float", "bool"], str) 515 ? capitalize(str) 516 : str; 517 518 str = str == "__type" ? "__Type" : str; 519 str = str == "__schema" ? "__Schema" : str; 520 str = str == "__inputvalue" ? "__InputValue" : str; 521 str = str == "__directive" ? "__Directive" : str; 522 str = str == "__field" ? "__Field" : str; 523 524 ret.arr = arr; 525 526 ret.str = str; 527 //writefln("%s %s", __LINE__, ret); 528 529 //return old == str ? Nullable!(StringTypeStrip).init : nullable(ret); 530 return nullable(ret); 531 } 532 533 unittest { 534 auto a = gqldStringTypeStrip("String"); 535 assert(!a.isNull()); 536 537 a = gqldStringTypeStrip("String!"); 538 assert(!a.isNull()); 539 assert(a.get().str == "String"); 540 assert(a.get().innerNotNull, format("%s", a.get())); 541 542 a = gqldStringTypeStrip("[String!]"); 543 assert(!a.isNull()); 544 assert(a.get().str == "String"); 545 assert(a.get().arr, format("%s", a.get())); 546 assert(a.get().innerNotNull, format("%s", a.get())); 547 548 a = gqldStringTypeStrip("[String]!"); 549 assert(!a.isNull()); 550 assert(a.get().str == "String"); 551 assert(a.get().arr, format("%s", a.get())); 552 assert(!a.get().innerNotNull, format("%s", a.get())); 553 assert(a.get().outerNotNull, format("%s", a.get())); 554 555 a = gqldStringTypeStrip("[String!]!"); 556 assert(!a.isNull()); 557 assert(a.get().str == "String"); 558 assert(a.get().arr, format("%s", a.get())); 559 assert(a.get().innerNotNull, format("%s", a.get())); 560 assert(a.get().outerNotNull, format("%s", a.get())); 561 } 562 563 private StringTypeStrip dlangStringTypeStrip(string str) { 564 StringTypeStrip ret; 565 ret.outerNotNull = true; 566 ret.innerNotNull = true; 567 ret.input = str; 568 569 immutable ns = "NullableStore!"; 570 immutable ns1 = "NullableStore!("; 571 immutable leaf = "GQLDCustomLeaf!"; 572 immutable leaf1 = "GQLDCustomLeaf!("; 573 immutable nll = "Nullable!"; 574 immutable nll1 = "Nullable!("; 575 576 // NullableStore!( .... ) 577 if(str.startsWith(ns1) && str.endsWith(")")) { 578 str = str[ns1.length .. $ - 1]; 579 } 580 581 // NullableStore!.... 582 if(str.startsWith(ns)) { 583 str = str[ns.length .. $]; 584 } 585 586 // GQLDCustomLeaf!( .... ) 587 if(str.startsWith(leaf1) && str.endsWith(")")) { 588 str = str[leaf1.length .. $ - 1]; 589 } 590 591 bool firstNull; 592 593 // Nullable!( .... ) 594 if(str.startsWith(nll1) && str.endsWith(")")) { 595 firstNull = true; 596 str = str[nll1.length .. $ - 1]; 597 } 598 599 // NullableStore!( .... ) 600 if(str.startsWith(ns1) && str.endsWith(")")) { 601 str = str[ns1.length .. $ - 1]; 602 } 603 604 // NullableStore!.... 605 if(str.startsWith(ns)) { 606 str = str[ns.length .. $]; 607 } 608 609 if(str.endsWith("!")) { 610 str = str[0 .. $ - 1]; 611 } 612 613 // xxxxxxx[] 614 if(str.endsWith("[]")) { 615 ret.arr = true; 616 str = str[0 .. $ - 2]; 617 } 618 619 bool secondNull; 620 621 // Nullable!( .... ) 622 if(str.startsWith(nll1) && str.endsWith(")")) { 623 secondNull = true; 624 str = str[nll1.length .. $ - 1]; 625 } 626 627 if(str.endsWith("!")) { 628 str = str[0 .. $ - 1]; 629 } 630 631 // Nullable! .... 632 if(str.startsWith(nll)) { 633 secondNull = true; 634 str = str[nll.length .. $]; 635 } 636 637 // NullableStore!( .... ) 638 if(str.startsWith(ns1) && str.endsWith(")")) { 639 str = str[ns1.length .. $ - 1]; 640 } 641 642 // NullableStore!.... 643 if(str.startsWith(ns)) { 644 str = str[ns.length .. $]; 645 } 646 647 str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str) 648 ? "Int" 649 : str; 650 651 str = canFind(["string", "int", "float", "bool"], str) 652 ? capitalize(str) 653 : str; 654 655 str = str == "__type" ? "__Type" : str; 656 str = str == "__schema" ? "__Schema" : str; 657 str = str == "__inputvalue" ? "__InputValue" : str; 658 str = str == "__directive" ? "__Directive" : str; 659 str = str == "__field" ? "__Field" : str; 660 661 //writefln("firstNull %s, secondNull %s, arr %s", firstNull, secondNull, 662 // ret.arr); 663 664 if(ret.arr) { 665 ret.innerNotNull = !secondNull; 666 ret.outerNotNull = !firstNull; 667 } else { 668 ret.innerNotNull = !secondNull; 669 } 670 671 ret.str = str; 672 return ret; 673 } 674 675 unittest { 676 bool oNN; 677 bool arr; 678 bool iNN; 679 string t = "Nullable!string"; 680 StringTypeStrip r = t.dlangStringTypeStrip(); 681 assert(r.str == "String", to!string(r)); 682 assert(!r.arr, to!string(r)); 683 assert(!r.innerNotNull, to!string(r)); 684 assert(r.outerNotNull, to!string(r)); 685 686 t = "Nullable!(string[])"; 687 r = t.dlangStringTypeStrip(); 688 assert(r.str == "String", to!string(r)); 689 assert(r.arr, to!string(r)); 690 assert(r.innerNotNull, to!string(r)); 691 assert(!r.outerNotNull, to!string(r)); 692 } 693 694 unittest { 695 string t = "Nullable!__type"; 696 StringTypeStrip r = t.dlangStringTypeStrip(); 697 assert(r.str == "__Type", to!string(r)); 698 assert(!r.innerNotNull, to!string(r)); 699 assert(r.outerNotNull, to!string(r)); 700 assert(!r.arr, to!string(r)); 701 702 t = "Nullable!(__type[])"; 703 r = t.dlangStringTypeStrip(); 704 assert(r.str == "__Type", to!string(r)); 705 assert(r.innerNotNull, to!string(r)); 706 assert(!r.outerNotNull, to!string(r)); 707 assert(r.arr, to!string(r)); 708 } 709 710 template isClass(T) { 711 enum isClass = is(T == class); 712 } 713 714 unittest { 715 static assert(!isClass!int); 716 static assert( isClass!Object); 717 } 718 719 template isNotInTypeSet(T, R...) { 720 import std.meta : staticIndexOf; 721 enum isNotInTypeSet = staticIndexOf!(T, R) == -1; 722 } 723 724 string getTypename(Schema,T)(auto ref T input) @trusted { 725 //pragma(msg, T); 726 //writefln("To %s", T.stringof); 727 static if(!isClass!(T)) { 728 return T.stringof; 729 } else { 730 // fetch the typeinfo of the item, and compare it down until we get to a 731 // class we have. If none found, return the name of the type itself. 732 import graphql.reflection; 733 auto tinfo = typeid(input); 734 auto reflect = SchemaReflection!Schema.instance; 735 while(tinfo !is null) { 736 if(auto cname = tinfo in reflect.classes) { 737 return *cname; 738 } 739 tinfo = tinfo.base; 740 } 741 return T.stringof; 742 } 743 } 744 745 Json toGraphqlJson(Schema,T)(auto ref T input) { 746 import std.array : empty; 747 import std.conv : to; 748 import std.typecons : Nullable; 749 import std.traits : isArray, isAggregateType, isBasicType, isSomeString, 750 isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple; 751 752 import nullablestore; 753 754 import graphql.uda : GQLDCustomLeaf; 755 static if(isArray!T && !isSomeString!T) { 756 Json ret = Json.emptyArray(); 757 foreach(ref it; input) { 758 ret ~= toGraphqlJson!Schema(it); 759 } 760 return ret; 761 } else static if(is(T : GQLDCustomLeaf!Type, Type...)) { 762 return Json(Type[1](input)); 763 } else static if(is(T : Nullable!Type, Type)) { 764 return input.isNull() ? Json(null) : toGraphqlJson!Schema(input.get()); 765 } else static if(is(T == enum)) { 766 return Json(to!string(input)); 767 } else static if(isBasicType!T || isScalarType!T || isSomeString!T) { 768 return serializeToJson(input); 769 } else static if(isAggregateType!T) { 770 Json ret = Json.emptyObject(); 771 772 // the important bit is the setting of the __typename field 773 ret["__typename"] = getTypename!(Schema)(input); 774 //writefln("Got %s", ret["__typename"].to!string()); 775 776 alias names = FieldNameTuple!(T); 777 alias types = FieldTypeTuple!(T); 778 static foreach(idx; 0 .. names.length) {{ 779 static if(!names[idx].empty) { 780 static if(is(types[idx] : NullableStore!Type, Type)) { 781 } else static if(is(types[idx] == enum)) { 782 ret[names[idx]] = 783 to!string(__traits(getMember, input, names[idx])); 784 } else { 785 ret[names[idx]] = toGraphqlJson!Schema( 786 __traits(getMember, input, names[idx]) 787 ); 788 } 789 } 790 }} 791 return ret; 792 } else { 793 static assert(false, T.stringof ~ " not supported"); 794 } 795 } 796 797 string dtToString(DateTime dt) { 798 return dt.toISOExtString(); 799 } 800 801 string dToString(Date dt) { 802 return dt.toISOExtString(); 803 } 804 805 unittest { 806 import std.typecons : nullable, Nullable; 807 import graphql.uda; 808 import nullablestore; 809 810 struct Foo { 811 int a; 812 Nullable!int b; 813 NullableStore!float c; 814 GQLDCustomLeaf!(DateTime, dtToString) dt2; 815 Nullable!(GQLDCustomLeaf!(DateTime, dtToString)) dt; 816 } 817 818 DateTime dt = DateTime(1337, 7, 1, 1, 1, 1); 819 DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3); 820 821 Foo foo; 822 foo.dt2 = GQLDCustomLeaf!(DateTime, dtToString)(dt2); 823 foo.dt = nullable(GQLDCustomLeaf!(DateTime, dtToString)(dt)); 824 Json j = toGraphqlJson!int(foo); 825 assert(j["a"].to!int() == 0); 826 assert(j["b"].type == Json.Type.null_); 827 assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type, 828 j.toPrettyString() 829 ) 830 ); 831 string exp = j["dt"].to!string(); 832 assert(exp == "1337-07-01T01:01:01", exp); 833 string exp2 = j["dt2"].to!string(); 834 assert(exp2 == "2337-07-01T01:01:03", exp2); 835 } 836 837 struct PathElement { 838 string str; 839 size_t idx; 840 841 static PathElement opCall(string s) { 842 PathElement ret; 843 ret.str = s; 844 return ret; 845 } 846 847 static PathElement opCall(size_t s) { 848 PathElement ret; 849 ret.idx = s; 850 return ret; 851 } 852 853 Json toJson() { 854 return this.str.empty ? Json(this.idx) : Json(this.str); 855 } 856 } 857