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