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 				"data" in objectValue ? objectValue["data"] : objectValue,
352 				arguments, context, ec
353 			);
354 		} catch(GQLDExecutionException e) {
355 			auto ret = Json.emptyObject();
356 			ret[Constants.data] = Json(null);
357 			ret[Constants.errors] = Json.emptyArray();
358 			ret.insertError(e.msg, ec.path);
359 			return ret;
360 		}
361 
362 		auto retType = this.schema.getReturnType(objectType,
363 				field.aka.empty ? field.name : field.aka
364 			);
365 		if(retType is null) {
366 			this.executationTraceLog.logf("ERR %s %s", objectType.name,
367 					field.name
368 				);
369 			Json ret = Json.emptyObject();
370 			ret[Constants.errors] = Json.emptyArray();
371 			ret.insertError(format(
372 					"No return type for member '%s' of type '%s' found",
373 					field.name, objectType.name
374 				));
375 			return ret;
376 		}
377 		this.executationTraceLog.logf("retType %s, de: %s", retType.name, de);
378 		return this.executeSelectionSet(field.f.ss, retType, de, variables,
379 				doc, context, ec
380 			);
381 	}
382 
383 	Json executeSelectionSet(SelectionSet ss, GQLDType objectType,
384 			Json objectValue, Json variables, Document doc, ref Con context,
385 			ref ExecutionContext ec)
386 	{
387 		Json rslt;
388 		if(GQLDMap map = objectType.toMap()) {
389 			this.executationTraceLog.log("MMMMMAP %s %s", map.name, ss !is null);
390 			enforce(ss !is null && ss.sel !is null, format(
391 					"ss null %s, ss.sel null %s", ss is null,
392 					(ss is null) ? true : ss.sel is null));
393 			rslt = this.executeSelections(ss.sel, map, objectValue, variables,
394 					doc, context, ec
395 				);
396 		} else if(GQLDNonNull nonNullType = objectType.toNonNull()) {
397 			this.executationTraceLog.logf("NonNull %s objectValue %s",
398 					nonNullType.elementType.name, objectValue
399 				);
400 			rslt = this.executeSelectionSet(ss, nonNullType.elementType,
401 					objectValue, variables, doc, context, ec
402 				);
403 			if(rslt.dataIsNull()) {
404 				this.executationTraceLog.logf("%s", rslt);
405 				rslt.insertError("NonNull was null", ec.path);
406 			}
407 		} else if(GQLDNullable nullType = objectType.toNullable()) {
408 			this.executationTraceLog.logf("NNNNULLABLE %s %s", nullType.name,
409 					objectValue);
410 			this.executationTraceLog.logf("IIIIIS EMPTY %s objectValue %s",
411 					objectValue.dataIsEmpty(), objectValue
412 				);
413 			if(objectValue.type == Json.Type.null_ ||
414 				(objectValue.type == Json.Type.object &&
415 				((nullType.elementType.toList && "data" !in objectValue)
416 				 || objectValue.dataIsNull))) {
417 				if(objectValue.type != Json.Type.object) {
418 					objectValue = Json.emptyObject();
419 				}
420 				objectValue["data"] = null;
421 				objectValue.remove(Constants.errors);
422 				rslt = objectValue;
423 			} else {
424 				rslt = this.executeSelectionSet(ss, nullType.elementType,
425 						objectValue, variables, doc, context, ec
426 					);
427 			}
428 		} else if(GQLDList list = objectType.toList()) {
429 			this.executationTraceLog.logf("LLLLLIST %s objectValue %s",
430 					list.name, objectValue);
431 			rslt = this.executeList(ss, list, objectValue, variables, doc,
432 					context, ec
433 				);
434 		} else if(GQLDScalar scalar = objectType.toScalar()) {
435 			rslt = objectValue;
436 		}
437 
438 		return rslt;
439 	}
440 
441 	private void toRun(SelectionSet ss, GQLDType elemType, Json item,
442 			Json variables, ref Json ret, Document doc, ref Con context,
443 			ref ExecutionContext ec)
444 		@trusted
445 	{
446 		this.executationTraceLog.logf("ET: %s, item %s", elemType.name,
447 				item
448 			);
449 		Json tmp = this.executeSelectionSet(ss, elemType, item, variables,
450 				doc, context, ec
451 			);
452 		if(tmp.type == Json.Type.object) {
453 			if("data" in tmp) {
454 				ret["data"] ~= tmp["data"];
455 			}
456 			foreach(err; tmp[Constants.errors]) {
457 				ret[Constants.errors] ~= err;
458 			}
459 		} else if(!tmp.dataIsEmpty() && tmp.isScalar()) {
460 			ret["data"] ~= tmp;
461 		}
462 	}
463 
464 	Json executeList(SelectionSet ss, GQLDList objectType,
465 			Json objectValue, Json variables, Document doc, ref Con context,
466 			ref ExecutionContext ec)
467 			@trusted
468 	{
469 		this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s",
470 				objectType.name, objectValue, variables
471 			);
472 		assert("data" in objectValue, objectValue.toString());
473 		GQLDType elemType = objectType.elementType;
474 		this.executationTraceLog.logf("elemType %s", elemType);
475 		Json ret = returnTemplate();
476 		ret["data"] = Json.emptyArray();
477 		if(this.options.asyncList == AsyncList.yes) {
478 			Task[] tasks;
479 			foreach(Json item;
480 					objectValue["data"].type == Json.Type.array
481 						? objectValue["data"]
482 						: Json.emptyArray()
483 				)
484 			{
485 				tasks ~= runTask({
486 					() nothrow {
487 					try {
488 						auto newEC = ec.dup;
489 						this.toRun(ss, elemType, item, variables, ret, doc,
490 								context, newEC
491 							);
492 					} catch(Exception e) {
493 						try {
494 							this.executationTraceLog.errorf("Error in task %s"
495 									, e.toString());
496 						} catch(Exception f) {
497 						}
498 					}
499 					}();
500 				});
501 			}
502 			foreach(task; tasks) {
503 				task.join();
504 			}
505 		} else {
506 			size_t idx;
507 			foreach(Json item;
508 					objectValue["data"].type == Json.Type.array
509 						? objectValue["data"]
510 						: Json.emptyArray()
511 				)
512 			{
513 				ec.path ~= PathElement(idx);
514 				++idx;
515 				scope(exit) {
516 					ec.path.popBack();
517 				}
518 				this.toRun(ss, elemType, item, variables, ret, doc, context, ec);
519 			}
520 		}
521 		return ret;
522 	}
523 }
524 
525 import graphql.uda;
526 import std.datetime : DateTime;
527 
528 private DateTime fromStringImpl(string s) {
529 	return DateTime.fromISOExtString(s);
530 }
531 
532 @GQLDUda(TypeKind.OBJECT)
533 private struct Query {
534 
535 	GQLDCustomLeaf!(DateTime, toStringImpl, fromStringImpl)  current();
536 }
537 
538 private class Schema {
539 	Query queryTyp;
540 }
541 
542 unittest {
543 	import graphql.schema.typeconversions;
544 	import graphql.traits;
545 	import std.datetime : DateTime;
546 
547 	alias a = collectTypes!(Schema);
548 	alias exp = AliasSeq!(Schema, Query, string, long, bool,
549 					GQLDCustomLeaf!(DateTime, toStringImpl, fromStringImpl));
550 	//static assert(is(a == exp), format("\n%s\n%s", a.stringof, exp.stringof));
551 
552 	//pragma(msg, InheritedClasses!Schema);
553 
554 	//auto g = new GraphQLD!(Schema,int)();
555 }