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 }