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 = "Boolean";
125 	}
126 
127 	override string toString() const {
128 		return "Boolean";
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 	this(string name) {
162 		super(GQLDKind.Object_);
163 		super.name = name;
164 	}
165 
166 	override string toString() const {
167 		return format(
168 				"Object %s(%s))\n\t\t\t\tBase(%s)\n\t\t\t\tDerivaties(%s)",
169 				this.name,
170 				this.member
171 					.byKeyValue
172 					.map!(kv => format("%s %s", kv.key,
173 							kv.value.toShortString()))
174 					.joiner(",\n\t\t\t\t"),
175 				(this.base !is null ? this.base.toShortString() : "null"),
176 				this.derivatives.map!(d => d.toShortString())
177 			);
178 	}
179 
180 	override string toShortString() const {
181 		return format("%s", super.name);
182 	}
183 }
184 
185 class GQLDUnion : GQLDMap {
186 	this(string name) {
187 		super(GQLDKind.Union);
188 		super.name = name;
189 	}
190 
191 	override string toString() const {
192 		return format("Union %s(%s))\n\t\t\t\tDerivaties(%s)",
193 				this.name,
194 				this.member
195 					.byKeyValue
196 					.map!(kv => format("%s %s", kv.key,
197 							kv.value.toShortString()))
198 					.joiner(",\n\t\t\t\t"),
199 				this.derivatives.map!(d => d.toShortString())
200 			);
201 	}
202 }
203 
204 class GQLDList : GQLDType {
205 	GQLDType elementType;
206 
207 	this(GQLDType elemType) {
208 		super(GQLDKind.List);
209 		super.name = "List";
210 		this.elementType = elemType;
211 	}
212 
213 	override string toString() const {
214 		return format("List(%s)", this.elementType.toShortString());
215 	}
216 
217 	override string toShortString() const {
218 		return format("List(%s)", this.elementType.toShortString());
219 	}
220 }
221 
222 class GQLDNonNull : GQLDType {
223 	GQLDType elementType;
224 
225 	this(GQLDType elemType) {
226 		super(GQLDKind.NonNull);
227 		super.name = "NonNull";
228 		this.elementType = elemType;
229 	}
230 
231 	override string toString() const {
232 		return format("NonNull(%s)", this.elementType.toShortString());
233 	}
234 
235 	override string toShortString() const {
236 		return format("NonNull(%s)", this.elementType.toShortString());
237 	}
238 }
239 
240 class GQLDNullable : GQLDType {
241 	GQLDType elementType;
242 
243 	this(GQLDType elemType) {
244 		super(GQLDKind.Nullable);
245 		super.name = "Nullable";
246 		this.elementType = elemType;
247 	}
248 
249 	override string toString() const {
250 		return format("Nullable(%s)", this.elementType.toShortString());
251 	}
252 
253 	override string toShortString() const {
254 		return format("Nullable(%s)", this.elementType.toShortString());
255 	}
256 }
257 
258 class GQLDOperation : GQLDType {
259 	GQLDType returnType;
260 	string returnTypeName;
261 
262 	GQLDType[string] parameters;
263 
264 	this(GQLDKind kind) {
265 		super(kind);
266 	}
267 
268 	override string toString() const {
269 		return format("%s %s(%s)", super.kind, returnType.toShortString(),
270 				this.parameters
271 					.byKeyValue
272 					.map!(kv =>
273 						format("%s %s", kv.key, kv.value.toShortString())
274 					)
275 					.joiner(", ")
276 				);
277 	}
278 }
279 
280 class GQLDQuery : GQLDOperation {
281 	this() {
282 		super(GQLDKind.Query);
283 		super.name = "Query";
284 	}
285 }
286 
287 class GQLDMutation : GQLDOperation {
288 	this() {
289 		super(GQLDKind.Mutation);
290 		super.name = "Mutation";
291 	}
292 }
293 
294 class GQLDSubscription : GQLDOperation {
295 	this() {
296 		super(GQLDKind.Subscription);
297 		super.name = "Subscription";
298 	}
299 }
300 
301 class GQLDSchema(Type) : GQLDMap {
302 	GQLDType[string] types;
303 
304 	GQLDObject __schema;
305 	GQLDObject __type;
306 	GQLDObject __field;
307 	GQLDObject __inputValue;
308 	GQLDObject __enumValue;
309 	GQLDObject __directives;
310 
311 	GQLDNonNull __nonNullType;
312 	GQLDNullable __nullableType;
313 	GQLDNullable __listOfNonNullType;
314 	GQLDNonNull __nonNullListOfNonNullType;
315 	GQLDNonNull __nonNullField;
316 	GQLDNullable __listOfNonNullField;
317 	GQLDNonNull __nonNullInputValue;
318 	GQLDList __listOfNonNullInputValue;
319 	GQLDNonNull __nonNullListOfNonNullInputValue;
320 	GQLDList __listOfNonNullEnumValue;
321 
322 	this() {
323 		super(GQLDKind.Schema);
324 		super.name = "Schema";
325 		this.createInbuildTypes();
326 		this.createIntrospectionTypes();
327 	}
328 
329 	void createInbuildTypes() {
330 		this.types["string"] = new GQLDString();
331 		foreach(t; ["String", "Int", "Float", "Boolean"]) {
332 			GQLDObject tmp = new GQLDObject(t);
333 			this.types[t] = tmp;
334 			tmp.member[Constants.name] = new GQLDString();
335 			tmp.member[Constants.description] = new GQLDString();
336 			tmp.member[Constants.kind] = new GQLDEnum(Constants.__TypeKind);
337 			//tmp.resolver = buildTypeResolver!(Type,Con)();
338 		}
339 	}
340 
341 	void createIntrospectionTypes() {
342 		// build base types
343 		auto str = new GQLDString();
344 		auto nnStr = new GQLDNonNull(str);
345 		auto nllStr = new GQLDNullable(str);
346 
347 		auto b = new GQLDBool();
348 		auto nnB = new GQLDNonNull(b);
349 		this.__schema = new GQLDObject(Constants.__schema);
350 		this.__type = new GQLDObject(Constants.__Type);
351 		this.__nullableType = new GQLDNullable(this.__type);
352 		this.__schema.member["mutationType"] = this.__nullableType;
353 		this.__schema.member["subscriptionType"] = this.__nullableType;
354 
355 		this.__type.member[Constants.ofType] = this.__nullableType;
356 		this.__type.member[Constants.kind] = new GQLDEnum(Constants.__TypeKind);
357 		this.__type.member[Constants.name] = nllStr;
358 		this.__type.member[Constants.description] = nllStr;
359 
360 		this.__nonNullType = new GQLDNonNull(this.__type);
361 		this.__schema.member["queryType"] = this.__nonNullType;
362 		auto lNNTypes = new GQLDList(this.__nonNullType);
363 		auto nlNNTypes = new GQLDNullable(lNNTypes);
364 		this.__listOfNonNullType = new GQLDNullable(lNNTypes);
365 		this.__type.member[Constants.interfaces] = new GQLDNonNull(lNNTypes);
366 		this.__type.member["possibleTypes"] = nlNNTypes;
367 
368 		this.__nonNullListOfNonNullType = new GQLDNonNull(lNNTypes);
369 		this.__schema.member["types"] = this.__nonNullListOfNonNullType;
370 
371 		this.__field = new GQLDObject("__Field");
372 		this.__field.member[Constants.name] = nnStr;
373 		this.__field.member[Constants.description] = nllStr;
374 		this.__field.member[Constants.type] = this.__nonNullType;
375 		this.__field.member[Constants.isDeprecated] = nnB;
376 		this.__field.member[Constants.deprecationReason] = nllStr;
377 
378 		this.__nonNullField = new GQLDNonNull(this.__field);
379 		auto lNNFields = new GQLDList(this.__nonNullField);
380 		this.__listOfNonNullField = new GQLDNullable(lNNFields);
381 		this.__type.member[Constants.fields] = this.__listOfNonNullField;
382 
383 		this.__inputValue = new GQLDObject(Constants.__InputValue);
384 		this.__inputValue.member[Constants.name] = nnStr;
385 		this.__inputValue.member[Constants.description] = nllStr;
386 		this.__inputValue.member["defaultValue"] = nllStr;
387 		this.__inputValue.member[Constants.type] = this.__nonNullType;
388 
389 		this.__nonNullInputValue = new GQLDNonNull(this.__inputValue);
390 		this.__listOfNonNullInputValue = new GQLDList(
391 				this.__nonNullInputValue
392 			);
393 		auto nlNNInputValue = new GQLDNullable(
394 				this.__listOfNonNullInputValue
395 			);
396 
397 		this.__type.member["inputFields"] = nlNNInputValue;
398 
399 		this.__nonNullListOfNonNullInputValue = new GQLDNonNull(
400 				this.__listOfNonNullInputValue
401 			);
402 
403 		this.__field.member[Constants.args] = this.__nonNullListOfNonNullInputValue;
404 
405 		this.__enumValue = new GQLDObject(Constants.__EnumValue);
406 		this.__enumValue.member[Constants.name] = nnStr;
407 		this.__enumValue.member[Constants.description] = nllStr;
408 		this.__enumValue.member[Constants.isDeprecated] = nnB;
409 		this.__enumValue.member[Constants.deprecationReason] = nllStr;
410 
411 		this.__listOfNonNullEnumValue = new GQLDList(new GQLDNonNull(
412 				this.__enumValue
413 			));
414 
415 		auto nnListOfNonNullEnumValue = new
416 			GQLDNullable(this.__listOfNonNullEnumValue);
417 
418 		//this.__type.member[Constants.enumValues] = this.__listOfNonNullEnumValue;
419 		this.__type.member[Constants.enumValues] = nnListOfNonNullEnumValue;
420 
421 		this.__directives = new GQLDObject(Constants.__Directive);
422 		this.__directives.member[Constants.name] = nnStr;
423 		this.__directives.member[Constants.description] = str;
424 		this.__directives.member[Constants.args] =
425 			this.__nonNullListOfNonNullInputValue;
426 		this.__directives.member[Constants.locations] = new GQLDNonNull(
427 				this.__listOfNonNullEnumValue
428 			);
429 
430 		this.__schema.member[Constants.directives] = new GQLDNonNull(
431 				new GQLDList(new GQLDNonNull(this.__directives))
432 			);
433 
434 
435 		foreach(t; ["String", "Int", "Float", "Boolean"]) {
436 			this.types[t].toObject().member[Constants.fields] =
437 				this.__listOfNonNullField;
438 		}
439 	}
440 
441 	override string toString() const {
442 		auto app = appender!string();
443 		formattedWrite(app, "Operation\n");
444 		foreach(key, value; super.member) {
445 			formattedWrite(app, "%s: %s\n", key, value.toString());
446 		}
447 
448 		formattedWrite(app, "Types\n");
449 		foreach(key, value; this.types) {
450 			formattedWrite(app, "%s: %s\n", key, value.toString());
451 		}
452 		return app.data;
453 	}
454 
455 	GQLDType getReturnType(GQLDType t, string field) {
456 		GQLDType ret;
457 		GQLDObject ob = t.toObject();
458 		if(auto s = t.toScalar()) {
459 			ret = s;
460 			goto retLabel;
461 		} else if(auto op = t.toOperation()) {
462 			ret = op.returnType;
463 			goto retLabel;
464 		} else if(auto map = t.toMap()) {
465 			if(field in map.member) {
466 				auto tmp = map.member[field];
467 				if(auto op = tmp.toOperation()) {
468 					ret = op.returnType;
469 					goto retLabel;
470 				} else {
471 					ret = tmp;
472 					goto retLabel;
473 				}
474 			} else if(ob && ob.base && field in ob.base.member) {
475 				return ob.base.member[field];
476 			} else if(field == "__typename") {
477 				// the type of the field __typename is always a string
478 				ret = this.types["string"];
479 				goto retLabel;
480 			} else {
481 				// if we couldn't find it in the passed map, maybe it is in some
482 				// of its derivatives
483 				foreach(deriv; map.derivatives) {
484 					if(field in deriv.member) {
485 						return deriv.member[field];
486 					}
487 				}
488 				return null;
489 			}
490 		} else {
491 			ret = t;
492 		}
493 		retLabel:
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 				enum uda = getUdaData!(Type, fieldNames[idx]);
605 				static if(uda.ignore != Ignore.yes) {
606 					static if(fieldNames[idx] != Constants.directives) {{
607 						r.member[fieldNames[idx]] =
608 							typeToGQLDType!(fieldTypes[idx])(ret);
609 					}}
610 				}
611 			}}
612 
613 			static if(is(Type == class)) {
614 				alias bct = BaseClassesTuple!(Type);
615 				static if(bct.length > 1) {
616 					auto d = cast(GQLDObject)typeToGQLDType!(bct[0])(
617 							ret
618 						);
619 					r.base = d;
620 					d.addDerivative(r);
621 
622 				}
623 				assert(bct.length > 1 ? r.base !is null : true);
624 			}
625 
626 			static foreach(mem; __traits(allMembers, Type)) {
627 				static if(!is(__traits(getMember, Type, mem))) {{ // not a type
628 					enum uda = getUdaData!(Type, mem);
629 					alias MemType = typeof(__traits(getMember, Type, mem));
630 					static if(uda.ignore != Ignore.yes) {
631 						static if(isCallable!MemType) {{
632 							GQLDOperation op = new GQLDQuery();
633 							r.member[mem] = op;
634 							op.returnType =
635 								typeToGQLDType!(ReturnType!(MemType))(ret);
636 
637 							alias paraNames = ParameterIdentifierTuple!(
638 									__traits(getMember, Type, mem)
639 							   );
640 							alias paraTypes = Parameters!(
641 								    __traits(getMember, Type, mem)
642 							   );
643 							static foreach(idx; 0 .. paraNames.length) {
644 								op.parameters[paraNames[idx]] =
645 									typeToGQLDType!(paraTypes[idx])(ret);
646 							}
647 						}}
648 					}
649 				}}
650 			}
651 		}
652 		return r;
653 	} else {
654 		static assert(false, Type.stringof);
655 	}
656 }