1 module graphql.schema.typeconversions; 2 3 import std.array : empty; 4 import std.algorithm.searching : canFind; 5 import std.conv : to; 6 import std.stdio; 7 import std.traits; 8 import std.meta; 9 import std.typecons : Nullable, nullable; 10 import std.range : ElementEncodingType; 11 12 import vibe.data.json; 13 14 import nullablestore; 15 16 import graphql.schema.types; 17 import graphql.traits; 18 import graphql.uda; 19 import graphql.constants; 20 21 @safe: 22 23 template typeToTypeEnum(Type) { 24 static if(isAggregateType!Type && hasUDA!(Type, GQLDUdaData) 25 && getUDAs!(Type, GQLDUdaData)[0].typeKind != TypeKind.UNDEFINED) 26 { 27 enum udas = getUDAs!(Type, GQLDUdaData); 28 static assert(udas.length == 1); 29 enum GQLDUdaData u = udas[0]; 30 enum typeToTypeEnum = to!string(u.typeKind); 31 } else static if(is(Type == enum)) { 32 enum typeToTypeEnum = "ENUM"; 33 } else static if(is(Type == bool)) { 34 enum typeToTypeEnum = "SCALAR"; 35 } else static if(is(Type : GQLDCustomLeaf!F, F)) { 36 enum typeToTypeEnum = "SCALAR"; 37 } else static if(isFloatingPoint!(Type)) { 38 enum typeToTypeEnum = "SCALAR"; 39 } else static if(isIntegral!(Type)) { 40 enum typeToTypeEnum = "SCALAR"; 41 } else static if(isSomeString!Type) { 42 enum typeToTypeEnum = "SCALAR"; 43 } else static if(is(Type == void)) { 44 enum typeToTypeEnum = "SCALAR"; 45 } else static if(is(Type == union)) { 46 enum typeToTypeEnum = "UNION"; 47 } else static if(isAggregateType!Type) { 48 enum typeToTypeEnum = "OBJECT"; 49 } else { 50 static assert(false, Type.stringof ~ " not handled"); 51 } 52 } 53 54 template typeToTypeName(Type) { 55 import graphql.uda : GQLDCustomLeaf; 56 static if(is(Type == enum)) { 57 enum typeToTypeName = Type.stringof; 58 } else static if(is(Type == bool)) { 59 enum typeToTypeName = "Boolean"; 60 } else static if(is(Type == GQLDCustomLeaf!Fs, Fs...)) { 61 enum typeToTypeName = Fs[0].stringof; 62 } else static if(isFloatingPoint!(Type)) { 63 enum typeToTypeName = "Float"; 64 } else static if(isIntegral!(Type)) { 65 enum typeToTypeName = "Int"; 66 } else static if(isSomeString!Type) { 67 enum typeToTypeName = "String"; 68 } else { 69 enum typeToTypeName = Type.stringof; 70 } 71 } 72 73 unittest { 74 import std.datetime : Date; 75 static assert(typeToTypeName!(GQLDCustomLeaf!Date) == "Date"); 76 } 77 78 template typeToParameterTypeName(Type) { 79 template level2(Type) { 80 static if(is(Type : Nullable!F, F)) { 81 enum level2 = typeToTypeName!F; 82 } else { 83 enum level2 = typeToTypeName!Type ~ "!"; 84 } 85 } 86 87 template level1(Type) { 88 static if(isArray!Type && !isSomeString!Type) { 89 enum level1 = "[" ~ level2!(ElementEncodingType!Type) ~ "]"; 90 } else { 91 enum level1 = typeToTypeName!Type; 92 } 93 } 94 95 template level0(Type) { 96 static if(is(Type : Nullable!F, F)) { 97 enum level0 = level1!F; 98 } else { 99 enum level0 = level1!Type ~ "!"; 100 } 101 } 102 103 template levelM1(Type) { 104 static if(is(Type : NullableStore!F, F)) { 105 enum levelM1 = level0!F; 106 } else { 107 enum levelM1 = level0!Type; 108 } 109 } 110 111 enum typeToParameterTypeName = levelM1!Type; 112 } 113 114 unittest { 115 import std.datetime : Date; 116 static assert(typeToParameterTypeName!(int) == "Int!"); 117 static assert(typeToParameterTypeName!(Nullable!int) == "Int"); 118 static assert(typeToParameterTypeName!(double) == "Float!"); 119 static assert(typeToParameterTypeName!(Nullable!double) == "Float"); 120 static assert(typeToParameterTypeName!(double[]) == "[Float!]!"); 121 static assert(typeToParameterTypeName!(Nullable!(double)[]) == "[Float]!"); 122 static assert(typeToParameterTypeName!(Nullable!(Nullable!(double)[])) == 123 "[Float]"); 124 static assert(typeToParameterTypeName!(GQLDCustomLeaf!Date) == "Date!"); 125 } 126 127 unittest { 128 enum AEnum { 129 one, 130 two, 131 three 132 } 133 static assert(typeToParameterTypeName!(AEnum) == "AEnum!"); 134 static assert(typeToParameterTypeName!(Nullable!AEnum) == "AEnum"); 135 static assert(typeToParameterTypeName!(Nullable!(AEnum)[]) == "[AEnum]!"); 136 static assert(typeToParameterTypeName!(Nullable!(Nullable!(AEnum)[])) == 137 "[AEnum]"); 138 } 139 140 template isScalarType(Type) { 141 static if(is(Type == bool)) { 142 enum isScalarType = true; 143 } else static if(is(Type == GQLDCustomLeaf!Fs, Fs...)) { 144 enum isScalarType = true; 145 } else static if(isFloatingPoint!(Type)) { 146 enum isScalarType = true; 147 } else static if(isIntegral!(Type)) { 148 enum isScalarType = true; 149 } else static if(isSomeString!Type) { 150 enum isScalarType = true; 151 } else static if(is(Type == enum)) { 152 enum isScalarType = true; 153 } else { 154 enum isScalarType = false; 155 } 156 } 157 158 template typeToFieldType(Type) { 159 static if(isArray!Type && !isSomeString!Type) { 160 enum typeToFieldType = "__listType"; 161 } else static if(is(Type : Nullable!F, F)) { 162 enum typeToFieldType = F.stringof; 163 } else static if(is(Type : NullableStore!F, F)) { 164 enum typeToFieldType = Type.TypeValue.stringof; 165 } else { 166 enum typeToFieldType = "__nonNullType"; 167 } 168 } 169 170 Json typeFields(T)() { 171 import graphql.uda; 172 static enum memsToIgnore = ["__ctor", "toString", "toHash", "opCmp", 173 "opEquals", "Monitor", "factory"]; 174 Json ret = Json.emptyArray(); 175 bool[string] fieldsAlreadyIn; 176 alias TplusParents = AliasSeq!(T, InheritedClasses!T); 177 static foreach(Type; TplusParents) {{ 178 static foreach(mem; __traits(allMembers, Type)) {{ 179 enum GQLDUdaData udaData = getUdaData!(Type, mem); 180 static if(!canFind(memsToIgnore, mem) 181 && udaData.ignore != Ignore.yes) 182 { 183 if(mem !in fieldsAlreadyIn) { 184 fieldsAlreadyIn[mem] = true; 185 Json tmp = Json.emptyObject(); 186 tmp[Constants.name] = mem; 187 // needed for interfacesForType 188 tmp[Constants.__typename] = "__Field"; 189 tmp[Constants.description] = udaData.description.getText() 190 .empty 191 ? Json(null) 192 : Json(udaData.description.getText()); 193 194 tmp[Constants.isDeprecated] = 195 udaData.deprecationInfo.isDeprecated == IsDeprecated.yes 196 ? true 197 : false; 198 199 tmp[Constants.deprecationReason] = 200 udaData.deprecationInfo.isDeprecated == IsDeprecated.yes 201 ? Json(udaData.deprecationInfo.deprecationReason) 202 : Json(null); 203 204 tmp[Constants.args] = Json.emptyArray(); 205 static if(isCallable!(__traits(getMember, Type, mem))) { 206 alias RT = ReturnType!(__traits(getMember, Type, mem)); 207 alias RTS = stripArrayAndNullable!RT; 208 //tmp[Constants.typenameOrig] = typeToTypeName!(RT); 209 tmp[Constants.typenameOrig] = typeToParameterTypeName!(RT); 210 211 // InputValue 212 alias paraNames = ParameterIdentifierTuple!( 213 __traits(getMember, Type, mem) 214 ); 215 alias paraTypes = Parameters!( 216 __traits(getMember, Type, mem) 217 ); 218 alias paraDefs = ParameterDefaults!( 219 __traits(getMember, Type, mem) 220 ); 221 static foreach(idx; 0 .. paraNames.length) {{ 222 Json iv = Json.emptyObject(); 223 iv[Constants.name] = paraNames[idx]; 224 // needed for interfacesForType 225 iv[Constants.__typename] = Constants.__InputValue; 226 iv[Constants.description] = Json(null); 227 static if(__traits(compiles, __traits(getAttributes, 228 Parameters!(__traits(getMember, Type, 229 mem))[idx .. idx + 1]))) 230 { 231 iv[Constants.description] = Json(null); 232 alias udad = __traits(getAttributes, 233 Parameters!(__traits(getMember, Type, 234 mem))[idx .. idx + 1]); 235 static if(udad.length == 1) { 236 enum F = udad[0]; 237 static if(is(typeof(F) == GQLDUdaData)) { 238 iv[Constants.description] = 239 F.description.text; 240 } 241 } 242 } 243 iv[Constants.typenameOrig] = 244 typeToParameterTypeName!(paraTypes[idx]); 245 static if(!is(paraDefs[idx] == void)) { 246 iv[Constants.defaultValue] = serializeToJson( 247 paraDefs[idx] 248 ) 249 .toString(); 250 } else { 251 iv[Constants.defaultValue] = Json(null); 252 } 253 tmp[Constants.args] ~= iv; 254 }} 255 } else { 256 tmp[Constants.typenameOrig] = 257 typeToParameterTypeName!( 258 //typeToTypeName!( 259 typeof(__traits(getMember, Type, mem)) 260 ); 261 } 262 ret ~= tmp; 263 } 264 } 265 }} 266 }} 267 //writefln("%s %s", __LINE__, ret.toPrettyString()); 268 return ret; 269 } 270 271 Json inputFields(Type)() { 272 Json ret = Json.emptyArray(); 273 alias types = FieldTypeTuple!Type; 274 alias names = FieldNameTuple!Type; 275 static foreach(idx; 0 .. types.length) {{ 276 enum GQLDUdaData udaData = getUdaData!(types[idx]); 277 Json tmp = Json.emptyObject(); 278 tmp[Constants.name] = names[idx]; 279 tmp[Constants.description] = udaData.description.getText().empty 280 ? Json(null) 281 : Json(udaData.description.getText()); 282 283 // needed for interfacesForType 284 tmp[Constants.__typename] = Constants.__InputValue; 285 286 //tmp[Constants.typenameOrig] = typeToTypeName!(types[idx]); 287 tmp[Constants.typenameOrig] = typeToParameterTypeName!(types[idx]); 288 tmp[Constants.defaultValue] = serializeToJson( 289 __traits(getMember, Type.init, names[idx]) 290 ); 291 ret ~= tmp; 292 }} 293 return ret; 294 } 295 296 Json emptyType() { 297 Json ret = Json.emptyObject(); 298 ret[Constants.name] = Json(null); 299 ret[Constants.description] = Json(null); 300 ret[Constants.fields] = Json(null); 301 ret[Constants.interfacesNames] = Json(null); 302 ret[Constants.possibleTypesNames] = Json(null); 303 ret[Constants.enumValues] = Json(null); 304 ret["ofType"] = Json(null); 305 return ret; 306 } 307 308 Json removeNonNullAndList(Json j) { 309 string t = j["kind"].get!string(); 310 if(t == "NON_NULL" || t == "LIST") { 311 return removeNonNullAndList(j["ofType"]); 312 } else { 313 return j; 314 } 315 } 316 317 // remove the top nullable to find out if we have a NON_NULL or not 318 Json typeToJson(Type,Schema)() { 319 static if(is(Type : Nullable!F, F)) { 320 return typeToJson1!(F,Schema,Type)(); 321 } else static if(is(Type : NullableStore!F, F)) { 322 return typeToJson1!(Type.TypeValue,Schema,Type)(); 323 } else { 324 Json ret = emptyType(); 325 ret["kind"] = "NON_NULL"; 326 ret[Constants.__typename] = "__Type"; 327 ret["ofType"] = typeToJson1!(Type,Schema,Type)(); 328 return ret; 329 } 330 } 331 332 // remove the array is present 333 Json typeToJson1(Type,Schema,Orig)() { 334 static if(isArray!Type && !isSomeString!Type && !is(Type == enum)) { 335 Json ret = emptyType(); 336 ret["kind"] = "LIST"; 337 ret[Constants.__typename] = "__Type"; 338 ret["ofType"] = typeToJson2!(ElementEncodingType!Type, Schema, Orig)(); 339 return ret; 340 } else { 341 return typeToJsonImpl!(Type, Schema, Orig)(); 342 } 343 } 344 345 // remove another nullable 346 Json typeToJson2(Type,Schema,Orig)() { 347 static if(is(Type : Nullable!F, F)) { 348 return typeToJsonImpl!(F, Schema, Orig)(); 349 } else static if(is(Type : NullableStore!F, F)) { 350 return typeToJsonImpl!(Type.TypeValue, Schema, Orig)(); 351 } else { 352 Json ret = emptyType(); 353 ret["kind"] = "NON_NULL"; 354 ret[Constants.__typename] = "__Type"; 355 ret["ofType"] = typeToJsonImpl!(Type, Schema, Orig)(); 356 return ret; 357 } 358 } 359 360 Json typeToJsonImpl(Type,Schema,Orig)() { 361 Json ret = Json.emptyObject(); 362 enum string kind = typeToTypeEnum!(stripArrayAndNullable!Type); 363 ret["kind"] = kind; 364 ret[Constants.__typename] = "__Type"; 365 ret[Constants.name] = typeToTypeName!Type; 366 367 enum GQLDUdaData udaData = getUdaData!(Type); 368 enum des = udaData.description.text; 369 ret[Constants.description] = des.empty 370 ? Json(null) 371 : Json(des); 372 373 ret[Constants.isDeprecated] = 374 udaData.deprecationInfo.isDeprecated == IsDeprecated.yes 375 ? true 376 : false; 377 378 ret[Constants.deprecationReason] = 379 udaData.deprecationInfo.isDeprecated == IsDeprecated.yes 380 ? Json(udaData.deprecationInfo.deprecationReason) 381 : Json(null); 382 383 // fields 384 static if((is(Type == class) || is(Type == interface) || is(Type == struct)) 385 && !is(Type : Nullable!K, K) && !is(Type : NullableStore!K, K) 386 && !is(Type : GQLDCustomLeaf!Ks, Ks...)) 387 { 388 ret[Constants.fields] = typeFields!Type(); 389 } else { 390 ret[Constants.fields] = Json(null); 391 } 392 393 // inputFields 394 static if(kind == Constants.INPUT_OBJECT) { 395 ret[Constants.inputFields] = inputFields!Type(); 396 } else { 397 ret[Constants.inputFields] = Json(null); 398 } 399 400 // needed to resolve interfaces 401 static if(is(Type == class) || is(Type == interface)) { 402 ret[Constants.interfacesNames] = Json.emptyArray(); 403 static foreach(interfaces; InheritedClasses!Type) {{ 404 ret[Constants.interfacesNames] ~= interfaces.stringof; 405 }} 406 } else { 407 ret[Constants.interfacesNames] = Json(null); 408 } 409 410 // needed to resolve possibleTypes 411 static if(is(Type == class) || is(Type == union) 412 || is(Type == interface)) 413 { 414 ret[Constants.possibleTypesNames] = Json.emptyArray(); 415 alias PT = PossibleTypes!(Type, Schema); 416 static foreach(pt; PT) { 417 ret[Constants.possibleTypesNames] ~= pt.stringof; 418 } 419 } else { 420 ret[Constants.possibleTypesNames] = Json(null); 421 } 422 423 // enumValues 424 static if(is(Type == enum)) { 425 ret[Constants.enumValues] = Json.emptyArray(); 426 static foreach(mem; EnumMembers!Type) {{ 427 Json tmp = Json.emptyObject(); 428 tmp[Constants.__TypeKind] = Constants.__EnumValue; 429 tmp[Constants.name] = Json(to!string(mem)); 430 tmp[Constants.description] = "ENUM_DESCRIPTION_TODO"; 431 tmp[Constants.isDeprecated] = false; 432 tmp[Constants.deprecationReason] = "ENUM_DEPRECATIONREASON_TODO"; 433 ret[Constants.enumValues] ~= tmp; 434 }} 435 } else { 436 ret[Constants.enumValues] = Json(null); 437 } 438 439 // needed to resolve ofType 440 static if(is(Type : Nullable!F, F)) { 441 ret[Constants.ofTypeName] = F.stringof; 442 } else static if(is(Type : NullableStore!F, F)) { 443 ret[Constants.ofTypeName] = F.stringof; 444 } else static if(is(Type : GQLDCustomLeaf!Fs, Fs...)) { 445 ret[Constants.ofTypeName] = Fs[0].stringof; 446 } else static if(isArray!Type) { 447 ret[Constants.ofTypeName] = ElementEncodingType!(Type).stringof; 448 } 449 450 return ret; 451 } 452 453 @safe unittest { 454 import std.format : format; 455 Json r = typeToJson!(string,void)(); 456 Json e = parseJsonString(` 457 { 458 "__typename": "__Type", 459 "possibleTypesNames": null, 460 "enumValues": null, 461 "interfacesNames": null, 462 "kind": "NON_NULL", 463 "name": null, 464 "ofType": { 465 "__typename": "__Type", 466 "possibleTypesNames": null, 467 "enumValues": null, 468 "interfacesNames": null, 469 "kind": "SCALAR", 470 "isDeprecated": false, 471 "deprecationReason": null, 472 "name": "String", 473 "description": null, 474 "inputFields": null, 475 "ofTypeName": "immutable(char)", 476 "fields": null 477 }, 478 "description": null, 479 "fields": null 480 } 481 `); 482 assert(r == e, format("exp:\n%s\ngot:\n%s", e.toPrettyString(), 483 r.toPrettyString())); 484 } 485 486 @safe unittest { 487 enum FooBar { 488 foo, 489 bar 490 } 491 492 import std.format : format; 493 Json r = typeToJson!(FooBar,void)(); 494 Json e = parseJsonString(` 495 { 496 "__typename": "__Type", 497 "possibleTypesNames": null, 498 "enumValues": null, 499 "interfacesNames": null, 500 "kind": "NON_NULL", 501 "name": null, 502 "ofType": { 503 "__typename": "__Type", 504 "possibleTypesNames": null, 505 "enumValues": [ 506 { 507 "description": "ENUM_DESCRIPTION_TODO", 508 "deprecationReason": "ENUM_DEPRECATIONREASON_TODO", 509 "__TypeKind": "__EnumValue", 510 "isDeprecated": false, 511 "name": "foo" 512 }, 513 { 514 "description": "ENUM_DESCRIPTION_TODO", 515 "deprecationReason": "ENUM_DEPRECATIONREASON_TODO", 516 "__TypeKind": "__EnumValue", 517 "isDeprecated": false, 518 "name": "bar" 519 } 520 ], 521 "interfacesNames": null, 522 "kind": "ENUM", 523 "isDeprecated": false, 524 "deprecationReason": null, 525 "name": "FooBar", 526 "description": null, 527 "inputFields": null, 528 "fields": null 529 }, 530 "description": null, 531 "fields": null 532 } 533 `); 534 assert(r == e, format("exp:\n%s\ngot:\n%s", e.toPrettyString(), 535 r.toPrettyString())); 536 } 537 @safe unittest { 538 import std.format : format; 539 Json r = typeToJson!(Nullable!string,void)(); 540 Json e = parseJsonString(` 541 { 542 "__typename": "__Type", 543 "possibleTypesNames": null, 544 "enumValues": null, 545 "interfacesNames": null, 546 "kind": "SCALAR", 547 "isDeprecated": false, 548 "deprecationReason": null, 549 "name": "String", 550 "description": null, 551 "inputFields": null, 552 "ofTypeName": "immutable(char)", 553 "fields": null 554 } 555 `); 556 assert(r == e, format("exp:\n%s\ngot:\n%s", e.toPrettyString(), 557 r.toPrettyString())); 558 } 559 560 @safe unittest { 561 import std.format : format; 562 Json r = typeToJson!(Nullable!string,void)(); 563 Json e = parseJsonString(` 564 { 565 "__typename": "__Type", 566 "possibleTypesNames": null, 567 "enumValues": null, 568 "interfacesNames": null, 569 "kind": "SCALAR", 570 "isDeprecated": false, 571 "deprecationReason": null, 572 "name": "String", 573 "description": null, 574 "inputFields": null, 575 "ofTypeName": "immutable(char)", 576 "fields": null 577 } 578 `); 579 assert(r == e, format("exp:\n%s\ngot:\n%s", e.toPrettyString(), 580 r.toPrettyString())); 581 } 582 583 Json directivesToJson(Directives)() { 584 import std.string : stripLeft; 585 Json ret = Json.emptyArray(); 586 static enum memsToIgnore = ["__ctor", "toString", "toHash", "opCmp", 587 "opEquals", "Monitor", "factory"]; 588 alias TplusParents = AliasSeq!(Directives, InheritedClasses!Directives); 589 static foreach(Type; TplusParents) {{ 590 static foreach(mem; __traits(allMembers, Type)) {{ 591 static if(!canFind(memsToIgnore, mem)) { 592 Json tmp = Json.emptyObject(); 593 enum GQLDUdaData udaData = getUdaData!(Type, mem); 594 tmp[Constants.name] = mem; 595 // needed for interfacesForType 596 tmp[Constants.__typename] = Constants.__Directive; 597 tmp[Constants.description] = udaData.description.getText().empty 598 ? Json(null) 599 : Json(udaData.description.text); 600 601 tmp[Constants.isDeprecated] = 602 udaData.deprecationInfo.isDeprecated == IsDeprecated.yes 603 ? true 604 : false; 605 606 tmp[Constants.deprecationReason] = 607 udaData.deprecationInfo.isDeprecated == IsDeprecated.yes 608 ? Json(udaData.deprecationInfo.deprecationReason) 609 : Json(null); 610 611 //tmp[Constants.description] = Json(null); 612 tmp[Constants.locations] = Json.emptyArray(); 613 tmp[Constants.args] = Json.emptyArray(); 614 static if(isCallable!(__traits(getMember, Type, mem))) { 615 // InputValue 616 alias paraNames = ParameterIdentifierTuple!( 617 __traits(getMember, Type, mem) 618 ); 619 alias paraTypes = Parameters!( 620 __traits(getMember, Type, mem) 621 ); 622 alias paraDefs = ParameterDefaults!( 623 __traits(getMember, Type, mem) 624 ); 625 static foreach(idx; 0 .. paraNames.length) {{ 626 Json iv = Json.emptyObject(); 627 // TODO remove the strip left. Its in because the 628 // two default directives of GraphQL skip and include 629 // both have one parameter named "if". 630 iv[Constants.name] = stripLeft(paraNames[idx], "_"); 631 iv[Constants.description] = Json(null); 632 // needed for interfacesForType 633 iv[Constants.__typename] = Constants.__InputValue; 634 iv[Constants.typenameOrig] = typeToTypeName!(paraTypes[idx]); 635 //iv[Constants.typenameOrig] = typeToParameterTypeName!(paraTypes[idx]); 636 static if(!is(paraDefs[idx] == void)) { 637 iv[Constants.defaultValue] = serializeToJson(paraDefs[idx]) 638 .toString(); 639 } else { 640 iv[Constants.defaultValue] = Json(null); 641 } 642 tmp[Constants.args] ~= iv; 643 }} 644 } 645 ret ~= tmp; 646 } 647 }} 648 }} 649 return ret; 650 } 651 652 Json getField(Json j, string name) { 653 import graphql.constants; 654 655 if(j.type != Json.Type.object || Constants.fields !in j 656 || j[Constants.fields].type != Json.Type.array) 657 { 658 return Json.init; 659 } 660 661 foreach(it; j[Constants.fields].byValue) { 662 string itName = it[Constants.name].get!string(); 663 if(itName == name) { 664 return it; 665 } 666 } 667 return Json.init; 668 } 669 670 Json getIntrospectionField(string name) { 671 import std.format : format; 672 Json ret = Json.emptyObject(); 673 ret[Constants.typenameOrig] = name == Constants.__typename 674 ? "String" 675 : name == Constants.__schema 676 ? "__Schema" 677 : name == Constants.__type 678 ? "__Type" 679 : format("Not known introspection name '%s'", name); 680 return ret; 681 }