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