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