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 }