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.format : format;
8 
9 import vibe.data.json;
10 
11 import graphql.builder;
12 import graphql.ast;
13 import graphql.helper;
14 import graphql.tokenmodule;
15 import graphql.argumentextractor;
16 import graphql.schema.types;
17 import graphql.schema.resolver;
18 
19 @safe:
20 
21 struct DefaultContext {
22 }
23 
24 class GraphQLD(T, QContext = DefaultContext) {
25 	alias Con = QContext;
26 	alias QueryResolver = Json delegate(string name, Json parent,
27 			Json args, ref Con context) @safe;
28 
29 	alias Schema = GQLDSchema!(T);
30 
31 	Schema schema;
32 
33 	// the logger to use
34 	Logger executationTraceLog;
35 	Logger defaultResolverLog;
36 	Logger resolverLog;
37 
38 	// [Type][field]
39 	QueryResolver[string][string] resolver;
40 	QueryResolver defaultResolver;
41 
42 	this() {
43 		this.schema = toSchema!(T)();
44 		this.executationTraceLog = new MultiLogger(LogLevel.off);
45 		this.defaultResolverLog = new MultiLogger(LogLevel.off);
46 		this.resolverLog = new MultiLogger(LogLevel.off);
47 
48 		this.defaultResolver = delegate(string name, Json parent, Json args,
49 									ref Con context)
50 			{
51 				import std.format;
52 				this.defaultResolverLog.logf("name: %s, parent: %s, args: %s",
53 						name, parent, args
54 					);
55 				Json ret = Json.emptyObject();
56 				if(parent.type != Json.Type.null_ && name in parent) {
57 					ret["data"] = Json.emptyObject();
58 					ret["data"] = parent[name];
59 				} else {
60 					ret["error"] = Json.emptyArray();
61 					ret["error"] = Json(format("no field name '%s' found",
62 										name)
63 									);
64 				}
65 				this.defaultResolverLog.logf("default ret %s", ret);
66 				return ret;
67 			};
68 
69 		setDefaultSchemaResolver(this);
70 		writeln(this.schema.toString());
71 	}
72 
73 	void setResolver(string first, string second, QueryResolver resolver) {
74 		if(first !in this.resolver) {
75 			this.resolver[first] = QueryResolver[string].init;
76 		}
77 		this.resolver[first][second] = resolver;
78 	}
79 
80 	Json resolve(string type, string field, Json parent, Json args,
81 			ref Con context)
82 	{
83 		Json defaultArgs = this.getDefaultArguments(type, field);
84 		Json joinedArgs = joinJson(args, defaultArgs);
85 		this.resolverLog.logf("%s %s %s %s %s %s", type, field,
86 				defaultArgs, parent, args, joinedArgs
87 			);
88 		if(type !in this.resolver) {
89 			return defaultResolver(field, parent, joinedArgs, context);
90 		} else if(field !in this.resolver[type]) {
91 			return defaultResolver(field, parent, joinedArgs, context);
92 		} else {
93 			return this.resolver[type][field](field, parent, joinedArgs,
94 					context
95 				);
96 		}
97 	}
98 
99 	Json getDefaultArgumentImpl(string typename, Type)(string type,
100 			string field)
101 	{
102 		static if(isAggregateType!Type) {
103 			if(typename == type) {
104 				switch(field) {
105 					static foreach(mem; __traits(allMembers, Type)) {
106 						static if(isCallable!(
107 								__traits(getMember, Type, mem))
108 							)
109 						{
110 							case mem: {
111 								alias parNames = ParameterIdentifierTuple!(
112 										__traits(getMember, Type, mem)
113 									);
114 								alias parDef = ParameterDefaultValueTuple!(
115 										__traits(getMember, Type, mem)
116 									);
117 
118 								Json ret = Json.emptyObject();
119 								static foreach(i; 0 .. parNames.length) {
120 									static if(!is(parDef[i] == void)) {
121 										ret[parNames[i]] =
122 											serializeToJson(parDef[i]);
123 									}
124 								}
125 								return ret;
126 							}
127 						}
128 					}
129 					default: break;
130 				}
131 			}
132 		}
133 		return Json.init;
134 	}
135 
136 	Json getDefaultArguments(string type, string field) {
137 		import graphql.traits : collectTypes;
138 		switch(type) {
139 			static foreach(Type; collectTypes!(T)) {{
140 				case Type.stringof: {
141 					Json tmp = getDefaultArgumentImpl!(Type.stringof, Type)(
142 							type, field
143 						);
144 					if(tmp.type != Json.Type.undefined
145 							&& tmp.type != Json.Type.null_)
146 					{
147 						return tmp;
148 					}
149 				}
150 			}}
151 			default: {}
152 		}
153 		// entryPoint == ["query", "mutation", "subscription"];
154 		switch(type) {
155 			static foreach(entryPoint; FieldNameTuple!T) {{
156 				case entryPoint: {
157 					Json tmp = getDefaultArgumentImpl!(entryPoint,
158 							typeof(__traits(getMember, T, entryPoint)))
159 						(type, field);
160 					if(tmp.type != Json.Type.undefined
161 							&& tmp.type != Json.Type.null_)
162 					{
163 						return tmp;
164 					}
165 				}
166 			}}
167 			default: break;
168 		}
169 		defaultRet:
170 		return Json.init;
171 	}
172 
173 	Json execute(Document doc, Json variables, ref Con context) @trusted {
174 		import std.algorithm.searching : canFind, find;
175 		OperationDefinition[] ops = this.getOperations(doc);
176 
177 		auto selSet = ops
178 			.find!(op => op.ruleSelection == OperationDefinitionEnum.SelSet);
179 		if(!selSet.empty) {
180 			if(ops.length > 1) {
181 				throw new Exception(
182 					"If SelectionSet the number of Operations must be 1"
183 					);
184 			}
185 			return this.executeOperation(selSet.front, variables, doc, context);
186 		}
187 
188 		Json ret = returnTemplate();
189 		foreach(op; ops) {
190 			Json tmp = this.executeOperation(op, variables, doc, context);
191 			this.executationTraceLog.logf("%s\n%s", ret, tmp);
192 			if(canFind([OperationDefinitionEnum.OT_N,
193 					OperationDefinitionEnum.OT_N_D,
194 					OperationDefinitionEnum.OT_N_V,
195 					OperationDefinitionEnum.OT_N_VD], op.ruleSelection))
196 			{
197 				if(tmp.type == Json.Type.object && "data" in tmp) {
198 					foreach(key, value; tmp["data"].byKeyValue()) {
199 						if(key in ret["data"]) {
200 							this.executationTraceLog.logf(
201 									"key %s already present", key
202 								);
203 							continue;
204 						}
205 						ret["data"][key] = value;
206 					}
207 				}
208 				foreach(err; tmp["error"]) {
209 					ret["error"] ~= err;
210 				}
211 			}
212 		}
213 		return ret;
214 	}
215 
216 	static OperationDefinition[] getOperations(Document doc) {
217 		import std.algorithm : map;
218 		return opDefRange(doc).map!(op => op.def.op).array;
219 	}
220 
221 	Json executeOperation(OperationDefinition op, Json variables,
222 			Document doc, ref Con context)
223 	{
224 		if(op.ruleSelection == OperationDefinitionEnum.SelSet
225 				|| op.ot.tok.type == TokenType.query)
226 		{
227 			return this.executeQuery(op, variables, doc, context);
228 		} else if(op.ot.tok.type == TokenType.mutation) {
229 			return this.executeMutation(op, variables, doc, context);
230 		} else if(op.ot.tok.type == TokenType.subscription) {
231 			assert(false, "Subscription not supported yet");
232 		}
233 		assert(false, "Unexpected");
234 	}
235 
236 	Json executeMutation(OperationDefinition op, Json variables,
237 			Document doc, ref Con context)
238 	{
239 		log("mutation");
240 		Json tmp = this.executeSelections(op.ss.sel,
241 				this.schema.member["mutationType"], Json.emptyObject(),
242 				variables, doc, context
243 			);
244 		return tmp;
245 	}
246 
247 	Json executeQuery(OperationDefinition op, Json variables, Document doc,
248 			ref Con context)
249 	{
250 		log("query");
251 		Json tmp = this.executeSelections(op.ss.sel,
252 				this.schema.member["queryType"],
253 				Json.emptyObject(), variables, doc, context
254 			);
255 		return tmp;
256 	}
257 
258 	Json executeSelections(Selections sel, GQLDType objectType,
259 			Json objectValue, Json variables, Document doc, ref Con context)
260 	{
261 		import graphql.traits : interfacesForType;
262 		Json ret = returnTemplate();
263 		this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s",
264 				objectType.name, objectValue, variables);
265 		this.executationTraceLog.logf("TN: %s", interfacesForType!(T)(
266 				objectValue
267 					.getWithDefault!string("data.__typename", "__typename")
268 			));
269 		foreach(FieldRangeItem field;
270 				fieldRangeArr(sel, doc, interfacesForType!(T)(objectValue
271 					.getWithDefault!string("data.__typename", "__typename")))
272 			)
273 		{
274 			Json rslt = this.executeFieldSelection(field, objectType,
275 					objectValue, variables, doc, context
276 				);
277 			ret.insertPayload(field.name, rslt);
278 		}
279 		return ret;
280 	}
281 
282 	Json executeFieldSelection(FieldRangeItem field, GQLDType objectType,
283 			Json objectValue, Json variables, Document doc, ref Con context)
284 	{
285 		this.executationTraceLog.logf("FRI: %s, OT: %s, OV: %s, VAR: %s",
286 				field.name, objectType.name, objectValue, variables
287 			);
288 		Json arguments = getArguments(field, variables);
289 		Json de = this.resolve(objectType.name, field.name,
290 				"data" in objectValue ? objectValue["data"] : objectValue,
291 				arguments, context
292 			);
293 		auto retType = this.schema.getReturnType(objectType, field.name);
294 		if(retType is null) {
295 			this.executationTraceLog.logf("ERR %s %s", objectType.name,
296 					field.name
297 				);
298 			Json ret = Json.emptyObject();
299 			ret["error"] = Json.emptyArray();
300 			ret["error"] ~= Json(format(
301 					"No return type for member '%s' of type '%s' found",
302 					field.name, objectType.name
303 				));
304 			return ret;
305 		}
306 		this.executationTraceLog.logf("retType %s, de: %s", retType.name, de);
307 		return this.executeSelectionSet(field.f.ss, retType, de, arguments,
308 				doc, context
309 			);
310 	}
311 
312 	Json executeSelectionSet(SelectionSet ss, GQLDType objectType,
313 			Json objectValue, Json variables, Document doc, ref Con context)
314 	{
315 		Json rslt;
316 		if(GQLDMap map = objectType.toMap()) {
317 			this.executationTraceLog.logf("map %s %s", map.name, ss !is null);
318 			rslt = this.executeSelections(ss.sel, map, objectValue, variables,
319 					doc, context
320 				);
321 		} else if(GQLDNonNull nonNullType = objectType.toNonNull()) {
322 			this.executationTraceLog.logf("NonNull %s",
323 					nonNullType.elementType.name
324 				);
325 			rslt = this.executeSelectionSet(ss, nonNullType.elementType,
326 					objectValue, variables, doc, context
327 				);
328 			if(rslt.dataIsNull()) {
329 				this.executationTraceLog.logf("%s", rslt);
330 				rslt["error"] ~= Json("NonNull was null");
331 			}
332 		} else if(GQLDNullable nullType = objectType.toNullable()) {
333 			this.executationTraceLog.logf("nullable %s", nullType.name);
334 			rslt = this.executeSelectionSet(ss, nullType.elementType,
335 					objectValue, variables, doc, context
336 				);
337 			this.executationTraceLog.logf("IIIIIS EMPTY %s rslt %s",
338 					rslt.dataIsEmpty(), rslt
339 				);
340 			if(rslt.dataIsEmpty()) {
341 				rslt["data"] = null;
342 				rslt.remove("error");
343 			} else {
344 			}
345 		} else if(GQLDList list = objectType.toList()) {
346 			this.executationTraceLog.logf("list %s", list.name);
347 			rslt = this.executeList(ss, list, objectValue, variables, doc,
348 					context
349 				);
350 		} else if(GQLDScalar scalar = objectType.toScalar()) {
351 			rslt = objectValue;
352 		}
353 
354 		return rslt;
355 	}
356 
357 	Json executeList(SelectionSet ss, GQLDList objectType,
358 			Json objectValue, Json variables, Document doc, ref Con context)
359 			@trusted
360 	{
361 		this.executationTraceLog.logf("OT: %s, OJ: %s, VAR: %s",
362 				objectType.name, objectValue, variables
363 			);
364 		assert("data" in objectValue, objectValue.toString());
365 		auto elemType = objectType.elementType;
366 		this.executationTraceLog.logf("elemType %s", elemType);
367 		Json ret = returnTemplate();
368 		ret["data"] = Json.emptyArray();
369 		foreach(Json item;
370 				objectValue["data"].type == Json.Type.array
371 					? objectValue["data"]
372 					: Json.emptyArray()
373 			)
374 		{
375 			this.executationTraceLog.logf("ET: %s, item %s", elemType.name,
376 					item
377 				);
378 			Json tmp = this.executeSelectionSet(ss, elemType, item, variables,
379 					doc, context
380 				);
381 			if(tmp.type == Json.Type.object) {
382 				if("data" in tmp) {
383 					ret["data"] ~= tmp["data"];
384 				}
385 				foreach(err; tmp["error"]) {
386 					ret["error"] ~= err;
387 				}
388 			} else if(!tmp.dataIsEmpty() && tmp.isScalar()) {
389 				ret["data"] ~= tmp;
390 			}
391 		}
392 		return ret;
393 	}
394 
395 	Json getArguments(FieldRangeItem item, Json variables) {
396 		auto ae = new ArgumentExtractor(variables);
397 		ae.accept(cast(const(Field))item.f);
398 		return ae.arguments;
399 	}
400 }