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(TypeQ, SCH)(ref SCH ret) { 564 alias TypeUQ = Unqual!TypeQ; 565 alias Type = TypeQ; 566 static if(is(Type == enum)) { 567 GQLDEnum r; 568 if(Type.stringof in ret.types) { 569 r = cast(GQLDEnum)ret.types[Type.stringof]; 570 } else { 571 r = new GQLDEnum(Type.stringof, [__traits(allMembers, Type)]); 572 ret.types[Type.stringof] = r; 573 } 574 return r; 575 } else static if(is(Type == bool) || is(TypeUQ == bool)) { 576 return new GQLDBool(); 577 } else static if(isFloatingPoint!(Type) || isFloatingPoint!(TypeUQ)) { 578 return new GQLDFloat(); 579 } else static if(isIntegral!(Type) || isIntegral!(TypeUQ)) { 580 return new GQLDInt(); 581 } else static if(isSomeString!Type) { 582 return new GQLDString(); 583 } else static if(is(Type == union)) { 584 GQLDUnion r; 585 if(Type.stringof in ret.types) { 586 r = cast(GQLDUnion)ret.types[Type.stringof]; 587 } else { 588 r = new GQLDUnion(Type.stringof); 589 ret.types[Type.stringof] = r; 590 591 alias fieldNames = FieldNameTuple!(Type); 592 alias fieldTypes = Fields!(Type); 593 static foreach(idx; 0 .. fieldNames.length) {{ 594 static if(fieldNames[idx] != Constants.directives) {{ 595 auto tmp = typeToGQLDType!(fieldTypes[idx])(ret); 596 r.member[fieldNames[idx]] = tmp; 597 598 if(GQLDMap tmpMap = tmp.toMap()) { 599 r.addDerivative(tmpMap); 600 } 601 }} 602 }} 603 } 604 return r; 605 } else static if(is(Type : Nullable!F, F)) { 606 return new GQLDNullable(typeToGQLDType!(F)(ret)); 607 } else static if(is(Type : GQLDCustomLeaf!Fs, Fs...)) { 608 return new GQLDLeaf(Fs[0].stringof); 609 } else static if(is(Type : NullableStore!F, F)) { 610 return new GQLDNullable(typeToGQLDType!(F)(ret)); 611 } else static if(isArray!Type) { 612 return new GQLDList(typeToGQLDType!(ElementEncodingType!Type)(ret)); 613 } else static if(isAggregateType!Type) { 614 import graphql.uda; 615 616 if(Type.stringof in ret.types) { 617 return cast(GQLDObject)ret.types[Type.stringof]; 618 } 619 620 enum tuda = getUdaData!Type; 621 622 GQLDObject r; 623 r = tuda.typeKind != TypeKind.UNDEFINED 624 ? new GQLDObject(Type.stringof, tuda.typeKind) 625 : new GQLDObject(Type.stringof); 626 ret.types[Type.stringof] = r; 627 628 alias fieldNames = FieldNameTuple!(Type); 629 alias fieldTypes = Fields!(Type); 630 static foreach(idx; 0 .. fieldNames.length) {{ 631 enum uda = getUdaData!(Type, fieldNames[idx]); 632 static if(uda.ignore != Ignore.yes) { 633 static if (fieldNames[idx] != Constants.directives) { 634 r.member[fieldNames[idx]] = 635 typeToGQLDType!(fieldTypes[idx])(ret); 636 static if(uda.ignoreForInput == IgnoreForInput.yes) { 637 r.outputOnlyMembers.insert(fieldNames[idx]); 638 } 639 } 640 } 641 }} 642 643 static if(is(Type == class)) { 644 alias bct = BaseClassesTuple!(Type); 645 static if(bct.length > 1) { 646 auto d = cast(GQLDObject)typeToGQLDType!(bct[0])( 647 ret 648 ); 649 r.base = d; 650 d.addDerivative(r); 651 652 } 653 assert(bct.length > 1 ? r.base !is null : true); 654 } 655 656 static foreach(mem; __traits(allMembers, Type)) {{ 657 // not a type 658 static if(!is(__traits(getMember, Type, mem))) { 659 enum uda = getUdaData!(Type, mem); 660 alias MemType = typeof(__traits(getMember, Type, mem)); 661 static if(uda.ignore != Ignore.yes && isCallable!MemType) { 662 GQLDOperation op = new GQLDQuery(); 663 r.member[mem] = op; 664 op.returnType = 665 typeToGQLDType!(ReturnType!(MemType))(ret); 666 667 alias paraNames = ParameterIdentifierTuple!( 668 __traits(getMember, Type, mem) 669 ); 670 alias paraTypes = Parameters!( 671 __traits(getMember, Type, mem) 672 ); 673 static foreach(idx; 0 .. paraNames.length) { 674 op.parameters[paraNames[idx]] = 675 typeToGQLDType!(paraTypes[idx])(ret); 676 } 677 static if(uda.ignoreForInput == IgnoreForInput.yes) { 678 r.outputOnlyMembers.insert(mem); 679 } 680 } 681 } 682 }} 683 return r; 684 } else { 685 static assert(false, Type.stringof); 686 } 687 }