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