1 module graphql.graphql;
2 
3 import std.array : array, front, empty;
4 import std.stdio;
5 import std.experimental.logger;
6 import std.traits;
7 import std.meta : AliasSeq;
8 import std.range.primitives : popBack;
9 import std.format : format;
10 import std.exception : enforce;
11 
12 import vibe.core.core;
13 import vibe.data.json;
14 
15 import graphql.argumentextractor;
16 import graphql.ast;
17 import graphql.builder;
18 import graphql.constants;
19 import graphql.directives;
20 import graphql.helper;
21 import graphql.schema.resolver;
22 import graphql.schema.types;
23 import graphql.tokenmodule;
24 
25 @safe:
26 
27 enum AsyncList {
28 	no,
29 	yes
30 }
31 
32 struct GQLDOptions {
33 	AsyncList asyncList;
34 }
35 
36 struct DefaultContext {
37 }
38 
39 class GQLDExecutionException : Exception {
40 	this(string msg, string f = __FILE__, size_t l = __LINE__) {
41 		super(msg, f, l);
42 		this.line = l;
43 	}
44 }
45 
46 private struct ExecutionContext {
47 	// path information
48 	PathElement[] path;
49 
50 	@property ExecutionContext dup() const {
51 		import std.algorithm.mutation : copy;
52 		ExecutionContext ret;
53 		ret.path = new PathElement[](this.path.length);
54 		this.path.copy(ret.path);
55 		return ret;
56 	}
57 }
58 
59 class GraphQLD(T, QContext = DefaultContext) {
60 	alias Con = QContext;
61 	alias QueryResolver = Json delegate(string name, Json parent,
62 			Json args, ref Con context) @safe;
63 	alias DefaultQueryResolver = Json delegate(string name, Json parent,
64 			Json args, ref Con context, ref ExecutionContext ec) @safe;
65 
66 	alias Schema = GQLDSchema!(T);
67 	immutable GQLDOptions options;
68 
69 	Schema schema;
70 
71 	// the logger to use
72 	Logger executationTraceLog;
73 	Logger defaultResolverLog;
74 	Logger resolverLog;
75 
76 	// [Type][field]
77 	QueryResolver[string][string] resolver;
78 	DefaultQueryResolver defaultResolver;
79 
80 	this(GQLDOptions options = GQLDOptions.init) {
81 		this.options = options;
82 		this.schema = toSchema!(T)();
83 		this.executationTraceLog = new MultiLogger(LogLevel.off);
84 		this.defaultResolverLog = new MultiLogger(LogLevel.off);
85 		this.resolverLog = new MultiLogger(LogLevel.off);
86 
87 		this.defaultResolver = delegate(string name, Json parent, Json args,
88 									ref Con context, ref ExecutionContext ec)
89 			{
90 				import std.format;
91 				this.defaultResolverLog.logf("name: %s, parent: %s, args: %s",
92 						name, parent, args
93 					);
94 				Json ret = Json.emptyObject();
95 				if(parent.type != Json.Type.null_ && name in parent) {
96 					ret["data"] = Json.emptyObject();
97 					ret["data"] = parent[name];
98 				} else {
99 					ret[Constants.errors] = Json.emptyArray();
100 					ret.insertError(format(
101 							"no field name '%s' found on type '%s'",
102 									name,
103 									parent.getWithDefault!string("__typename")
104 							), ec.path
105 						);
106 				}
107 				this.defaultResolverLog.logf("default ret %s", ret);
108 				return ret;
109 			};
110 
111 		setDefaultSchemaResolver(this);
112 	}
113 
114 	void setResolver(string first, string second, QueryResolver resolver) {
115 		import std.exception : enforce;
116 		if(first !in this.resolver) {
117 			this.resolver[first] = QueryResolver[string].init;
118 		}
119 		enforce(second !in this.resolver[first]);
120 		this.resolver[first][second] = resolver;
121 	}
122 
123 	Json resolve(string type, string field, Json parent, Json args,
124 			ref Con context, ref ExecutionContext ec)
125 	{
126 		Json defaultArgs = this.getDefaultArguments(type, field);
127 		Json joinedArgs = joinJson!(JoinJsonPrecedence.a)(args, defaultArgs);
128 		//this.resolverLog.logf(
129 		assert(type != "__type" && field != "__ofType",
130 				parent.toPrettyString());
131 		this.resolverLog.logf(
132 				"type: %s field: %s defArgs: %s par: %s args: %s %s", type,
133 				field, defaultArgs, parent, args, joinedArgs
134 			);
135 		if(type !in this.resolver) {
136 			return defaultResolver(field, parent, joinedArgs, context, ec);
137 		} else if(field !in this.resolver[type]) {
138 			return defaultResolver(field, parent, joinedArgs, context, ec);
139 		} else {
140 			return this.resolver[type][field](field, parent, joinedArgs,
141 					context
142 				);
143 		}
144 	}
145 
146 	Json getDefaultArgumentImpl(string typename, Type)(string type,
147 			string field)
148 	{
149 		static if(isAggregateType!Type) {
150 			if(typename == type) {
151 				switch(field) {
152 					static foreach(mem; __traits(allMembers, Type)) {
153 						static if(isCallable!(
154 								__traits(getMember, Type, mem))
155 							)
156 						{
157 							case mem: {
158 								alias parNames = ParameterIdentifierTuple!(
159 										__traits(getMember, Type, mem)
160 									);
161 								alias parDef = ParameterDefaultValueTuple!(
162 										__traits(getMember, Type, mem)
163 									);
164 
165 								Json ret = Json.emptyObject();
166 								static foreach(i; 0 .. parNames.length) {
167 									static if(!is(parDef[i] == void)) {
168 										ret[parNames[i]] =
169 											serializeToJson(parDef[i]);
170 									}
171 								}
172 								return ret;
173 							}
174 						}
175 					}
176 					default: break;
177 				}
178 			}
179 		}
180 		return Json.init;
181 	}
182 
183 	Json getDefaultArguments(string type, string field) {
184 		import graphql.traits : collectTypes;
185 		switch(type) {
186 			static foreach(Type; collectTypes!(T)) {{
187 				case Type.stringof: {
188 					Json tmp = getDefaultArgumentImpl!(Type.stringof, Type)(
189 							type, field
190 						);
191 					if(tmp.type != Json.Type.undefined
192 							&& tmp.type != Json.Type.null_)
193 					{
194 						return tmp;
195 					}
196 				}
197 			}}
198 			default: {}
199 		}
200 		// entryPoint == ["query", "mutation", "subscription"];
201 		switch(type) {
202 			static foreach(entryPoint; FieldNameTuple!T) {{
203 				case entryPoint: {
204 					Json tmp = getDefaultArgumentImpl!(entryPoint,
205 							typeof(__traits(getMember, T, entryPoint)))
206 						(type, field);
207 					if(tmp.type != Json.Type.undefined
208 							&& tmp.type != Json.Type.null_)
209 					{
210 						return tmp;
211 					}
212 				}
213 			}}
214 			default: break;
215 		}
216 		defaultRet:
217 		return Json.init;
218 	}
219 
220 	Json execute(Document doc, Json variables, ref Con context) @trusted {
221 		import std.algorithm.searching : canFind, find;
222 		OperationDefinition[] ops = this.getOperations(doc);
223 		ExecutionContext ec;
224 
225 		Json ret = Json.emptyObject();
226 		ret[Constants.data] = Json.emptyObject();
227 		foreach(op; ops) {
228 			Json tmp = this.executeOperation(op, variables, doc, context, ec);
229 			this.executationTraceLog.logf("%s\n%s\n%s", op.ruleSelection, ret,
230 					tmp);
231 			if(tmp.type == Json.Type.object && Constants.data in tmp) {
232 				foreach(key, value; tmp[Constants.data].byKeyValue()) {
233 					if(key in ret[Constants.data]) {
234 						this.executationTraceLog.logf(
235 								"key %s already present", key
236 							);
237 						continue;
238 					}
239 					ret[Constants.data][key] = value;
240 				}
241 			}
242 			this.executationTraceLog.logf("%s", tmp);
243 			if(Constants.errors in tmp && !tmp[Constants.errors].dataIsEmpty())
244 			{
245 				ret[Constants.errors] = Json.emptyArray();
246 			}
247 			foreach(err; tmp[Constants.errors]) {
248 				ret[Constants.errors] ~= err;
249 			}
250 		}
251 		return ret;
252 	}
253 
254 	static OperationDefinition[] getOperations(Document doc) {
255 		import std.algorithm.iteration : map;
256 		return opDefRange(doc).map!(op => op.def.op).array;
257 	}
258 
259 	Json executeOperation(OperationDefinition op, Json variables,
260 			Document doc, ref Con context, ref ExecutionContext ec)
261 	{
262 		ec.path ~= PathElement(
263 				op.name.value.empty ? "SelectionSet" : op.name.value);
264 		scope(exit) {
265 			ec.path.popBack();
266 		}
267 		bool dirSaysToContinue = continueAfterDirectives(op.d, variables);
268 		if(!dirSaysToContinue) {
269 			return returnTemplate();
270 		}
271 		if(op.ruleSelection == OperationDefinitionEnum.SelSet
272 				|| op.ot.tok.type == TokenType.query)
273 		{
274 			return this.executeQuery(op, variables, doc, context, ec);
275 		} else if(op.ot.tok.type == TokenType.mutation) {
276 			return this.executeMutation(op, variables, doc, context, ec);
277 		} else if(op.ot.tok.type == TokenType.subscription) {
278 			assert(false, "Subscription not supported yet");
279 		}
280 		assert(false, "Unexpected");
281 	}
282 
283 	Json executeMutation(OperationDefinition op, Json variables,
284 			Document doc, ref Con context, ref ExecutionContext ec)
285 	{
286 		this.executationTraceLog.log("mutation");
287 		Json tmp = this.executeSelections(op.ss.sel,
288 				this.schema.member["mutationType"], Json.emptyObject(),
289 				variables, doc, context, ec
290 			);
291 		return tmp;
292 	}
293 
294 	Json executeQuery(OperationDefinition op, Json variables, Document doc,
295 			ref Con context, ref ExecutionContext ec)
296 	{
297 		this.executationTraceLog.log("query");
298 		Json tmp = this.executeSelections(op.ss.sel,
299 				this.schema.member["queryType"],
300 				Json.emptyObject(), variables, doc, context, ec
301 			);
302 		return tmp;
303 	}
304 
305 	Json executeSelections(Selections sel, GQLDType objectType,
306 			Json objectValue, Json variables, Document doc, ref Con context,
307 			ref ExecutionContext ec)
308 	{
309 		import graphql.traits : interfacesForType;
310 		Json ret = returnTemplate();
311 		this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s",
312 				objectType.name, objectValue, variables);
313 		this.executationTraceLog.logf("TN: %s", interfacesForType!(T)(
314 				objectValue
315 					.getWithDefault!string("data.__typename", "__typename")
316 			));
317 		foreach(FieldRangeItem field;
318 				fieldRangeArr(
319 					sel,
320 					doc,
321 					interfacesForType!(T)(objectValue.getWithDefault!string(
322 							"data.__typename", "__typename")
323 						),
324 					variables)
325 			)
326 		{
327 			//Json args = getArguments(field, variables);
328 			bool dirSaysToContinue = continueAfterDirectives(
329 					field.f.dirs, variables);
330 
331 			Json rslt = dirSaysToContinue
332 				? this.executeFieldSelection(field, objectType,
333 						objectValue, variables, doc, context, ec
334 					)
335 				: Json.emptyObject();
336 
337 			const string name = field.f.name.name.value;
338 			ret.insertPayload(name, rslt);
339 		}
340 		return ret;
341 	}
342 
343 	Json executeFieldSelection(FieldRangeItem field, GQLDType objectType,
344 			Json objectValue, Json variables, Document doc, ref Con context,
345 			ref ExecutionContext ec)
346 	{
347 		ec.path ~= PathElement(field.f.name.name.value);
348 		scope(exit) {
349 			ec.path.popBack();
350 		}
351 		this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s",
352 		//this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s",
353 				field.name, objectType.name, objectValue, variables
354 			);
355 		Json arguments = getArguments(field, variables);
356 		//writefln("field %s\nobj %s\nvar %s\narg %s", field.name, objectValue,
357 		//		variables, arguments);
358 		Json de;
359 		try {
360 			de = this.resolve(objectType.name,
361 					field.aka.empty ? field.name : field.aka,
362 				"data" in objectValue ? objectValue["data"] : objectValue,
363 				arguments, context, ec
364 			);
365 		} catch(GQLDExecutionException e) {
366 			auto ret = Json.emptyObject();
367 			ret[Constants.data] = Json(null);
368 			ret[Constants.errors] = Json.emptyArray();
369 			ret.insertError(e.msg, ec.path);
370 			return ret;
371 		}
372 
373 		auto retType = this.schema.getReturnType(objectType,
374 				field.aka.empty ? field.name : field.aka
375 			);
376 		if(retType is null) {
377 			this.executationTraceLog.logf("ERR %s %s", objectType.name,
378 					field.name
379 				);
380 			Json ret = Json.emptyObject();
381 			ret[Constants.errors] = Json.emptyArray();
382 			ret.insertError(format(
383 					"No return type for member '%s' of type '%s' found",
384 					field.name, objectType.name
385 				));
386 			return ret;
387 		}
388 		this.executationTraceLog.logf("retType %s, de: %s", retType.name, de);
389 		return this.executeSelectionSet(field.f.ss, retType, de, variables,
390 				doc, context, ec
391 			);
392 	}
393 
394 	Json executeSelectionSet(SelectionSet ss, GQLDType objectType,
395 			Json objectValue, Json variables, Document doc, ref Con context,
396 			ref ExecutionContext ec)
397 	{
398 		Json rslt;
399 		if(GQLDMap map = objectType.toMap()) {
400 			this.executationTraceLog.log("MMMMMAP %s %s", map.name, ss !is null);
401 			enforce(ss !is null && ss.sel !is null, format(
402 					"ss null %s, ss.sel null %s", ss is null,
403 					(ss is null) ? true : ss.sel is null));
404 			rslt = this.executeSelections(ss.sel, map, objectValue, variables,
405 					doc, context, ec
406 				);
407 		} else if(GQLDNonNull nonNullType = objectType.toNonNull()) {
408 			this.executationTraceLog.logf("NonNull %s objectValue %s",
409 					nonNullType.elementType.name, objectValue
410 				);
411 			rslt = this.executeSelectionSet(ss, nonNullType.elementType,
412 					objectValue, variables, doc, context, ec
413 				);
414 			if(rslt.dataIsNull()) {
415 				this.executationTraceLog.logf("%s", rslt);
416 				rslt.insertError("NonNull was null", ec.path);
417 			}
418 		} else if(GQLDNullable nullType = objectType.toNullable()) {
419 			this.executationTraceLog.logf("NNNNULLABLE %s %s", nullType.name,
420 					objectValue);
421 			this.executationTraceLog.logf("IIIIIS EMPTY %s objectValue %s",
422 					objectValue.dataIsEmpty(), objectValue
423 				);
424 			if(objectValue.dataIsEmpty()) {
425 				if(objectValue.type != Json.Type.object) {
426 					objectValue = Json.emptyObject();
427 				}
428 				objectValue["data"] = null;
429 				objectValue.remove(Constants.errors);
430 				rslt = objectValue;
431 			} else {
432 				rslt = this.executeSelectionSet(ss, nullType.elementType,
433 						objectValue, variables, doc, context, ec
434 					);
435 			}
436 		} else if(GQLDList list = objectType.toList()) {
437 			this.executationTraceLog.logf("LLLLLIST %s objectValue %s",
438 					list.name, objectValue);
439 			rslt = this.executeList(ss, list, objectValue, variables, doc,
440 					context, ec
441 				);
442 		} else if(GQLDScalar scalar = objectType.toScalar()) {
443 			rslt = objectValue;
444 		}
445 
446 		return rslt;
447 	}
448 
449 	private void toRun(SelectionSet ss, GQLDType elemType, Json item,
450 			Json variables, ref Json ret, Document doc, ref Con context,
451 			ref ExecutionContext ec)
452 		@trusted
453 	{
454 		this.executationTraceLog.logf("ET: %s, item %s", elemType.name,
455 				item
456 			);
457 		Json tmp = this.executeSelectionSet(ss, elemType, item, variables,
458 				doc, context, ec
459 			);
460 		if(tmp.type == Json.Type.object) {
461 			if("data" in tmp) {
462 				ret["data"] ~= tmp["data"];
463 			}
464 			foreach(err; tmp[Constants.errors]) {
465 				ret[Constants.errors] ~= err;
466 			}
467 		} else if(!tmp.dataIsEmpty() && tmp.isScalar()) {
468 			ret["data"] ~= tmp;
469 		}
470 	}
471 
472 	Json executeList(SelectionSet ss, GQLDList objectType,
473 			Json objectValue, Json variables, Document doc, ref Con context,
474 			ref ExecutionContext ec)
475 			@trusted
476 	{
477 		this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s",
478 				objectType.name, objectValue, variables
479 			);
480 		assert("data" in objectValue, objectValue.toString());
481 		GQLDType elemType = objectType.elementType;
482 		this.executationTraceLog.logf("elemType %s", elemType);
483 		Json ret = returnTemplate();
484 		ret["data"] = Json.emptyArray();
485 		if(this.options.asyncList == AsyncList.yes) {
486 			Task[] tasks;
487 			foreach(Json item;
488 					objectValue["data"].type == Json.Type.array
489 						? objectValue["data"]
490 						: Json.emptyArray()
491 				)
492 			{
493 				tasks ~= runTask({
494 					auto newEC = ec.dup;
495 					this.toRun(ss, elemType, item, variables, ret, doc,
496 							context, newEC
497 						);
498 				});
499 			}
500 			foreach(task; tasks) {
501 				task.join();
502 			}
503 		} else {
504 			size_t idx;
505 			foreach(Json item;
506 					objectValue["data"].type == Json.Type.array
507 						? objectValue["data"]
508 						: Json.emptyArray()
509 				)
510 			{
511 				ec.path ~= PathElement(idx);
512 				++idx;
513 				scope(exit) {
514 					ec.path.popBack();
515 				}
516 				this.toRun(ss, elemType, item, variables, ret, doc, context, ec);
517 			}
518 		}
519 		return ret;
520 	}
521 }
522 
523 import graphql.uda;
524 
525 @GQLDUda(TypeKind.OBJECT)
526 private struct Query {
527 	import std.datetime : DateTime;
528 
529 	GQLDCustomLeaf!DateTime current();
530 }
531 
532 private class Schema {
533 	Query queryTyp;
534 }
535 
536 unittest {
537 	import graphql.schema.typeconversions;
538 	import graphql.traits;
539 	import std.datetime : DateTime;
540 
541 	alias a = collectTypes!(Schema);
542 	alias exp = AliasSeq!(Schema, Query, string, long, bool,
543 					GQLDCustomLeaf!(DateTime, toStringImpl));
544 	//static assert(is(a == exp), format("\n%s\n%s", a.stringof, exp.stringof));
545 
546 	//pragma(msg, InheritedClasses!Schema);
547 
548 	//auto g = new GraphQLD!(Schema,int)();
549 }