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