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 }