1 module graphql.schema.types; 2 3 import std.conv : to; 4 import std.array; 5 import std.meta; 6 import std.traits; 7 import std.typecons; 8 import std.algorithm.iteration : map, joiner; 9 import std.algorithm.searching : canFind; 10 import std.container : RedBlackTree; 11 import std.range : ElementEncodingType; 12 import std.format; 13 import std.string : strip; 14 import std.experimental.logger; 15 import std.stdio; 16 17 import vibe.data.json; 18 19 import nullablestore; 20 21 import graphql.helper; 22 import graphql.traits; 23 import graphql.constants; 24 import graphql.uda; 25 26 @safe: 27 28 enum GQLDKind { 29 SimpleScalar, 30 String, 31 Float, 32 Int, 33 Bool, 34 CustomLeaf, 35 Object_, 36 List, 37 Enum, 38 Map, 39 Nullable, 40 NonNull, 41 Union, 42 Query, 43 Mutation, 44 Subscription, 45 Schema 46 } 47 48 abstract class GQLDType { 49 const GQLDKind kind; 50 string name; 51 52 this(GQLDKind kind) { 53 this.kind = kind; 54 } 55 56 override string toString() const { 57 return "GQLDType"; 58 } 59 60 string toShortString() const { 61 return this.toString(); 62 } 63 } 64 65 class GQLDScalar : GQLDType { 66 this(GQLDKind kind = GQLDKind.SimpleScalar) { 67 super(kind); 68 } 69 } 70 71 class GQLDLeaf : GQLDScalar { 72 this(string name) { 73 super(GQLDKind.CustomLeaf); 74 super.name = name; 75 } 76 77 override string toString() const { 78 return format("GQLDCustomLeaf(%s)", this.name); 79 } 80 } 81 82 class GQLDString : GQLDScalar { 83 this() { 84 super(GQLDKind.String); 85 super.name = "String"; 86 } 87 88 override string toString() const { 89 return "String"; 90 } 91 } 92 93 class GQLDFloat : GQLDScalar { 94 this() { 95 super(GQLDKind.Float); 96 super.name = "Float"; 97 } 98 99 override string toString() const { 100 return "Float"; 101 } 102 } 103 104 class GQLDInt : GQLDScalar { 105 this() { 106 super(GQLDKind.Int); 107 super.name = "Int"; 108 } 109 110 override string toString() const { 111 return "Int"; 112 } 113 } 114 115 class GQLDEnum : GQLDScalar { 116 string enumName; 117 string[] memberNames; 118 // should this also grab the values, for integration with something like 119 // https://www.apollographql.com/docs/apollo-server/schema/scalars-enums/#internal-values ? 120 this(string enumName, string[] memberNames = []) { 121 super(GQLDKind.Enum); 122 this.enumName = enumName; 123 this.memberNames = memberNames; 124 super.name = enumName; 125 } 126 127 override string toString() const { 128 return this.enumName; 129 } 130 } 131 132 class GQLDBool : GQLDScalar { 133 this() { 134 super(GQLDKind.Bool); 135 super.name = "Boolean"; 136 } 137 138 override string toString() const { 139 return "Boolean"; 140 } 141 } 142 143 class GQLDMap : GQLDType { 144 GQLDType[string] member; 145 RedBlackTree!string outputOnlyMembers; 146 GQLDMap[] derivatives; 147 148 this() { 149 this(GQLDKind.Map); 150 } 151 this(GQLDKind kind) { 152 super(kind); 153 this.outputOnlyMembers = new RedBlackTree!string(); 154 } 155 156 void addDerivative(GQLDMap d) { 157 if(!canFind!((a,b) => a is b)(this.derivatives, d)) { 158 this.derivatives ~= d; 159 } 160 } 161 162 override string toString() const { 163 auto app = appender!string(); 164 foreach(key, value; this.member) { 165 formattedWrite(app, "%s: %s\n", key, value.toString()); 166 } 167 return app.data; 168 } 169 } 170 171 class GQLDObject : GQLDMap { 172 GQLDObject base; 173 TypeKind typeKind; 174 175 this(string name) { 176 super(GQLDKind.Object_); 177 super.name = name; 178 } 179 180 this(string name, TypeKind tk) { 181 this(name); 182 this.typeKind = tk; 183 } 184 185 override string toString() const { 186 return format( 187 "Object %s(%s))\n\t\t\t\tBase(%s)\n\t\t\t\tDerivaties(%s)", 188 this.name, 189 this.member 190 .byKeyValue 191 .map!(kv => format("%s %s", kv.key, 192 kv.value.toShortString())) 193 .joiner(",\n\t\t\t\t"), 194 (this.base !is null ? this.base.toShortString() : "null"), 195 this.derivatives.map!(d => d.toShortString()) 196 ); 197 } 198 199 override string toShortString() const { 200 return format("%s", super.name); 201 } 202 } 203 204 class GQLDUnion : GQLDMap { 205 this(string name) { 206 super(GQLDKind.Union); 207 super.name = name; 208 } 209 210 override string toString() const { 211 return format("Union %s(%s))\n\t\t\t\tDerivaties(%s)", 212 this.name, 213 this.member 214 .byKeyValue 215 .map!(kv => format("%s %s", kv.key, 216 kv.value.toShortString())) 217 .joiner(",\n\t\t\t\t"), 218 this.derivatives.map!(d => d.toShortString()) 219 ); 220 } 221 } 222 223 class GQLDList : GQLDType { 224 GQLDType elementType; 225 226 this(GQLDType elemType) { 227 super(GQLDKind.List); 228 super.name = "List"; 229 this.elementType = elemType; 230 } 231 232 override string toString() const { 233 return format("List(%s)", this.elementType.toShortString()); 234 } 235 236 override string toShortString() const { 237 return format("List(%s)", this.elementType.toShortString()); 238 } 239 } 240 241 class GQLDNonNull : GQLDType { 242 GQLDType elementType; 243 244 this(GQLDType elemType) { 245 super(GQLDKind.NonNull); 246 super.name = "NonNull"; 247 this.elementType = elemType; 248 } 249 250 override string toString() const { 251 return format("NonNull(%s)", this.elementType.toShortString()); 252 } 253 254 override string toShortString() const { 255 return format("NonNull(%s)", this.elementType.toShortString()); 256 } 257 } 258 259 class GQLDNullable : GQLDType { 260 GQLDType elementType; 261 262 this(GQLDType elemType) { 263 super(GQLDKind.Nullable); 264 super.name = "Nullable"; 265 this.elementType = elemType; 266 } 267 268 override string toString() const { 269 return format("Nullable(%s)", this.elementType.toShortString()); 270 } 271 272 override string toShortString() const { 273 return format("Nullable(%s)", this.elementType.toShortString()); 274 } 275 } 276 277 class GQLDOperation : GQLDType { 278 GQLDType returnType; 279 string returnTypeName; 280 281 GQLDType[string] parameters; 282 283 this(GQLDKind kind) { 284 super(kind); 285 } 286 287 override string toString() const { 288 return format("%s %s(%s)", super.kind, returnType.toShortString(), 289 this.parameters 290 .byKeyValue 291 .map!(kv => 292 format("%s %s", kv.key, kv.value.toShortString()) 293 ) 294 .joiner(", ") 295 ); 296 } 297 } 298 299 class GQLDQuery : GQLDOperation { 300 this() { 301 super(GQLDKind.Query); 302 super.name = "Query"; 303 } 304 } 305 306 class GQLDMutation : GQLDOperation { 307 this() { 308 super(GQLDKind.Mutation); 309 super.name = "Mutation"; 310 } 311 } 312 313 class GQLDSubscription : GQLDOperation { 314 this() { 315 super(GQLDKind.Subscription); 316 super.name = "Subscription"; 317 } 318 } 319 320 class GQLDSchema(Type) : GQLDMap { 321 GQLDType[string] types; 322 323 GQLDObject __schema; 324 GQLDObject __type; 325 GQLDObject __field; 326 GQLDObject __inputValue; 327 GQLDObject __enumValue; 328 GQLDObject __directives; 329 330 GQLDNonNull __nonNullType; 331 GQLDNullable __nullableType; 332 GQLDNullable __listOfNonNullType; 333 GQLDNonNull __nonNullListOfNonNullType; 334 GQLDNonNull __nonNullField; 335 GQLDNullable __listOfNonNullField; 336 GQLDNonNull __nonNullInputValue; 337 GQLDList __listOfNonNullInputValue; 338 GQLDNonNull __nonNullListOfNonNullInputValue; 339 GQLDList __listOfNonNullEnumValue; 340 341 this() { 342 super(GQLDKind.Schema); 343 super.name = "Schema"; 344 this.createInbuildTypes(); 345 this.createIntrospectionTypes(); 346 } 347 348 void createInbuildTypes() { 349 this.types["string"] = new GQLDString(); 350 foreach(t; ["String", "Int", "Float", "Boolean"]) { 351 GQLDObject tmp = new GQLDObject(t); 352 this.types[t] = tmp; 353 tmp.member[Constants.name] = new GQLDString(); 354 tmp.member[Constants.description] = new GQLDString(); 355 tmp.member[Constants.kind] = new GQLDEnum(Constants.__TypeKind); 356 //tmp.resolver = buildTypeResolver!(Type,Con)(); 357 } 358 } 359 360 void createIntrospectionTypes() { 361 // build base types 362 auto str = new GQLDString(); 363 auto nnStr = new GQLDNonNull(str); 364 auto nllStr = new GQLDNullable(str); 365 366 auto b = new GQLDBool(); 367 auto nnB = new GQLDNonNull(b); 368 this.__schema = new GQLDObject(Constants.__schema); 369 this.__type = new GQLDObject(Constants.__Type); 370 this.__nullableType = new GQLDNullable(this.__type); 371 this.__schema.member["mutationType"] = this.__nullableType; 372 this.__schema.member["subscriptionType"] = this.__nullableType; 373 374 this.__type.member[Constants.ofType] = this.__nullableType; 375 this.__type.member[Constants.kind] = new GQLDEnum(Constants.__TypeKind); 376 this.__type.member[Constants.name] = nllStr; 377 this.__type.member[Constants.description] = nllStr; 378 379 this.__nonNullType = new GQLDNonNull(this.__type); 380 this.__schema.member["queryType"] = this.__nonNullType; 381 auto lNNTypes = new GQLDList(this.__nonNullType); 382 auto nlNNTypes = new GQLDNullable(lNNTypes); 383 this.__listOfNonNullType = new GQLDNullable(lNNTypes); 384 this.__type.member[Constants.interfaces] = new GQLDNonNull(lNNTypes); 385 this.__type.member["possibleTypes"] = nlNNTypes; 386 387 this.__nonNullListOfNonNullType = new GQLDNonNull(lNNTypes); 388 this.__schema.member["types"] = this.__nonNullListOfNonNullType; 389 390 this.__field = new GQLDObject("__Field"); 391 this.__field.member[Constants.name] = nnStr; 392 this.__field.member[Constants.description] = nllStr; 393 this.__field.member[Constants.type] = this.__nonNullType; 394 this.__field.member[Constants.isDeprecated] = nnB; 395 this.__field.member[Constants.deprecationReason] = nllStr; 396 397 this.__nonNullField = new GQLDNonNull(this.__field); 398 auto lNNFields = new GQLDList(this.__nonNullField); 399 this.__listOfNonNullField = new GQLDNullable(lNNFields); 400 this.__type.member[Constants.fields] = this.__listOfNonNullField; 401 402 this.__inputValue = new GQLDObject(Constants.__InputValue); 403 this.__inputValue.member[Constants.name] = nnStr; 404 this.__inputValue.member[Constants.description] = nllStr; 405 this.__inputValue.member["defaultValue"] = nllStr; 406 this.__inputValue.member[Constants.type] = this.__nonNullType; 407 408 this.__nonNullInputValue = new GQLDNonNull(this.__inputValue); 409 this.__listOfNonNullInputValue = new GQLDList( 410 this.__nonNullInputValue 411 ); 412 auto nlNNInputValue = new GQLDNullable( 413 this.__listOfNonNullInputValue 414 ); 415 416 this.__type.member["inputFields"] = nlNNInputValue; 417 418 this.__nonNullListOfNonNullInputValue = new GQLDNonNull( 419 this.__listOfNonNullInputValue 420 ); 421 422 this.__field.member[Constants.args] = this.__nonNullListOfNonNullInputValue; 423 424 this.__enumValue = new GQLDObject(Constants.__EnumValue); 425 this.__enumValue.member[Constants.name] = nnStr; 426 this.__enumValue.member[Constants.description] = nllStr; 427 this.__enumValue.member[Constants.isDeprecated] = nnB; 428 this.__enumValue.member[Constants.deprecationReason] = nllStr; 429 430 this.__listOfNonNullEnumValue = new GQLDList(new GQLDNonNull( 431 this.__enumValue 432 )); 433 434 auto nnListOfNonNullEnumValue = new 435 GQLDNullable(this.__listOfNonNullEnumValue); 436 437 //this.__type.member[Constants.enumValues] = this.__listOfNonNullEnumValue; 438 this.__type.member[Constants.enumValues] = nnListOfNonNullEnumValue; 439 440 this.__directives = new GQLDObject(Constants.__Directive); 441 this.__directives.member[Constants.name] = nnStr; 442 this.__directives.member[Constants.description] = str; 443 this.__directives.member[Constants.args] = 444 this.__nonNullListOfNonNullInputValue; 445 this.__directives.member[Constants.locations] = new GQLDNonNull( 446 this.__listOfNonNullEnumValue 447 ); 448 449 this.__schema.member[Constants.directives] = new GQLDNonNull( 450 new GQLDList(new GQLDNonNull(this.__directives)) 451 ); 452 453 454 foreach(t; ["String", "Int", "Float", "Boolean"]) { 455 this.types[t].toObject().member[Constants.fields] = 456 this.__listOfNonNullField; 457 } 458 } 459 460 override string toString() const { 461 auto app = appender!string(); 462 formattedWrite(app, "Operation\n"); 463 foreach(key, value; super.member) { 464 formattedWrite(app, "%s: %s\n", key, value.toString()); 465 } 466 467 formattedWrite(app, "Types\n"); 468 foreach(key, value; this.types) { 469 formattedWrite(app, "%s: %s\n", key, value.toString()); 470 } 471 return app.data; 472 } 473 474 GQLDType getReturnType(GQLDType t, string field) { 475 GQLDType ret; 476 GQLDObject ob = t.toObject(); 477 if(auto s = t.toScalar()) { 478 ret = s; 479 goto retLabel; 480 } else if(auto op = t.toOperation()) { 481 ret = op.returnType; 482 goto retLabel; 483 } else if(auto map = t.toMap()) { 484 if(field in map.member) { 485 auto tmp = map.member[field]; 486 if(auto op = tmp.toOperation()) { 487 ret = op.returnType; 488 goto retLabel; 489 } else { 490 ret = tmp; 491 goto retLabel; 492 } 493 } else if(ob && ob.base && field in ob.base.member) { 494 return ob.base.member[field]; 495 } else if(field == "__typename") { 496 // the type of the field __typename is always a string 497 ret = this.types["string"]; 498 goto retLabel; 499 } else { 500 // if we couldn't find it in the passed map, maybe it is in some 501 // of its derivatives 502 foreach(deriv; map.derivatives) { 503 if(field in deriv.member) { 504 return deriv.member[field]; 505 } 506 } 507 return null; 508 } 509 } else { 510 ret = t; 511 } 512 retLabel: 513 return ret; 514 } 515 } 516 517 GQLDObject toObject(GQLDType t) { 518 return cast(typeof(return))t; 519 } 520 521 GQLDMap toMap(GQLDType t) { 522 return cast(typeof(return))t; 523 } 524 525 GQLDScalar toScalar(GQLDType t) { 526 return cast(typeof(return))t; 527 } 528 529 GQLDOperation toOperation(GQLDType t) { 530 return cast(typeof(return))t; 531 } 532 533 GQLDList toList(GQLDType t) { 534 return cast(typeof(return))t; 535 } 536 537 GQLDNullable toNullable(GQLDType t) { 538 return cast(typeof(return))t; 539 } 540 541 GQLDNonNull toNonNull(GQLDType t) { 542 return cast(typeof(return))t; 543 } 544 545 unittest { 546 auto str = new GQLDString(); 547 assert(str.name == "String"); 548 549 auto map = str.toMap(); 550 assert(map is null); 551 } 552 553 string toShortString(const(GQLDType) e) { 554 if(auto o = cast(const(GQLDObject))e) { 555 return o.name; 556 } else if(auto u = cast(const(GQLDUnion))e) { 557 return u.name; 558 } else { 559 return e.toString(); 560 } 561 } 562 563 GQLDType typeToGQLDType(Type, SCH)(ref SCH ret) { 564 static if(is(Type == enum)) { 565 GQLDEnum r; 566 if(Type.stringof in ret.types) { 567 r = cast(GQLDEnum)ret.types[Type.stringof]; 568 } else { 569 r = new GQLDEnum(Type.stringof, [__traits(allMembers, Type)]); 570 ret.types[Type.stringof] = r; 571 } 572 return r; 573 } else static if(is(Type == bool)) { 574 return new GQLDBool(); 575 } else static if(isFloatingPoint!(Type)) { 576 return new GQLDFloat(); 577 } else static if(isIntegral!(Type)) { 578 return new GQLDInt(); 579 } else static if(isSomeString!Type) { 580 return new GQLDString(); 581 } else static if(is(Type == union)) { 582 GQLDUnion r; 583 if(Type.stringof in ret.types) { 584 r = cast(GQLDUnion)ret.types[Type.stringof]; 585 } else { 586 r = new GQLDUnion(Type.stringof); 587 ret.types[Type.stringof] = r; 588 589 alias fieldNames = FieldNameTuple!(Type); 590 alias fieldTypes = Fields!(Type); 591 static foreach(idx; 0 .. fieldNames.length) {{ 592 static if(fieldNames[idx] != Constants.directives) {{ 593 auto tmp = typeToGQLDType!(fieldTypes[idx])(ret); 594 r.member[fieldNames[idx]] = tmp; 595 596 if(GQLDMap tmpMap = tmp.toMap()) { 597 r.addDerivative(tmpMap); 598 } 599 }} 600 }} 601 } 602 return r; 603 } else static if(is(Type : Nullable!F, F)) { 604 return new GQLDNullable(typeToGQLDType!(F)(ret)); 605 } else static if(is(Type : GQLDCustomLeaf!Fs, Fs...)) { 606 return new GQLDLeaf(Fs[0].stringof); 607 } else static if(is(Type : NullableStore!F, F)) { 608 return new GQLDNullable(typeToGQLDType!(F)(ret)); 609 } else static if(isArray!Type) { 610 return new GQLDList(typeToGQLDType!(ElementEncodingType!Type)(ret)); 611 } else static if(isAggregateType!Type) { 612 import graphql.uda; 613 614 if(Type.stringof in ret.types) { 615 return cast(GQLDObject)ret.types[Type.stringof]; 616 } 617 618 enum tuda = getUdaData!Type; 619 620 GQLDObject r; 621 r = tuda.typeKind != TypeKind.UNDEFINED 622 ? new GQLDObject(Type.stringof, tuda.typeKind) 623 : new GQLDObject(Type.stringof); 624 ret.types[Type.stringof] = r; 625 626 alias fieldNames = FieldNameTuple!(Type); 627 alias fieldTypes = Fields!(Type); 628 static foreach(idx; 0 .. fieldNames.length) {{ 629 enum uda = getUdaData!(Type, fieldNames[idx]); 630 static if(uda.ignore != Ignore.yes) { 631 static if (fieldNames[idx] != Constants.directives) { 632 r.member[fieldNames[idx]] = 633 typeToGQLDType!(fieldTypes[idx])(ret); 634 static if(uda.ignoreForInput == IgnoreForInput.yes) { 635 r.outputOnlyMembers.insert(fieldNames[idx]); 636 } 637 } 638 } 639 }} 640 641 static if(is(Type == class)) { 642 alias bct = BaseClassesTuple!(Type); 643 static if(bct.length > 1) { 644 auto d = cast(GQLDObject)typeToGQLDType!(bct[0])( 645 ret 646 ); 647 r.base = d; 648 d.addDerivative(r); 649 650 } 651 assert(bct.length > 1 ? r.base !is null : true); 652 } 653 654 static foreach(mem; __traits(allMembers, Type)) {{ 655 // not a type 656 static if(!is(__traits(getMember, Type, mem))) { 657 enum uda = getUdaData!(Type, mem); 658 alias MemType = typeof(__traits(getMember, Type, mem)); 659 static if(uda.ignore != Ignore.yes && isCallable!MemType) { 660 GQLDOperation op = new GQLDQuery(); 661 r.member[mem] = op; 662 op.returnType = 663 typeToGQLDType!(ReturnType!(MemType))(ret); 664 665 alias paraNames = ParameterIdentifierTuple!( 666 __traits(getMember, Type, mem) 667 ); 668 alias paraTypes = Parameters!( 669 __traits(getMember, Type, mem) 670 ); 671 static foreach(idx; 0 .. paraNames.length) { 672 op.parameters[paraNames[idx]] = 673 typeToGQLDType!(paraTypes[idx])(ret); 674 } 675 static if(uda.ignoreForInput == IgnoreForInput.yes) { 676 r.outputOnlyMembers.insert(mem); 677 } 678 } 679 } 680 }} 681 return r; 682 } else { 683 static assert(false, Type.stringof); 684 } 685 }