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