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