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 }