1 module graphql.schema.typeconversions;
2 
3 import std.array : empty;
4 import std.algorithm.searching : canFind;
5 import std.conv : to;
6 import std.stdio;
7 import std.traits;
8 import std.meta;
9 import std.typecons : Nullable, nullable;
10 import std.range : ElementEncodingType;
11 
12 import vibe.data.json;
13 
14 import nullablestore;
15 
16 import graphql.schema.types;
17 import graphql.traits;
18 import graphql.uda;
19 import graphql.constants;
20 
21 @safe:
22 
23 template typeToTypeEnum(Type) {
24 	static if(isAggregateType!Type && hasUDA!(Type, GQLDUdaData)
25 			&& getUDAs!(Type, GQLDUdaData)[0].typeKind != TypeKind.UNDEFINED)
26 	{
27 		enum udas = getUDAs!(Type, GQLDUdaData);
28 		static assert(udas.length == 1);
29 		enum GQLDUdaData u = udas[0];
30 		enum typeToTypeEnum = to!string(u.typeKind);
31 	} else static if(is(Type == enum)) {
32 		enum typeToTypeEnum = "ENUM";
33 	} else static if(is(Type == bool)) {
34 		enum typeToTypeEnum = "SCALAR";
35 	} else static if(is(Type : GQLDCustomLeaf!F, F)) {
36 		enum typeToTypeEnum = "SCALAR";
37 	} else static if(isFloatingPoint!(Type)) {
38 		enum typeToTypeEnum = "SCALAR";
39 	} else static if(isIntegral!(Type)) {
40 		enum typeToTypeEnum = "SCALAR";
41 	} else static if(isSomeString!Type) {
42 		enum typeToTypeEnum = "SCALAR";
43 	} else static if(is(Type == void)) {
44 		enum typeToTypeEnum = "SCALAR";
45 	} else static if(is(Type == union)) {
46 		enum typeToTypeEnum = "UNION";
47 	} else static if(isAggregateType!Type) {
48 		enum typeToTypeEnum = "OBJECT";
49 	} else {
50 		static assert(false, Type.stringof ~ " not handled");
51 	}
52 }
53 
54 template typeToTypeName(Type) {
55 	import graphql.uda : GQLDCustomLeaf;
56 	static if(is(Type == enum)) {
57 		enum typeToTypeName = Type.stringof;
58 	} else static if(is(Type == bool)) {
59 		enum typeToTypeName = "Bool";
60 	} else static if(is(Type == GQLDCustomLeaf!Fs, Fs...)) {
61 		enum typeToTypeName = Fs[0].stringof;
62 	} else static if(isFloatingPoint!(Type)) {
63 		enum typeToTypeName = "Float";
64 	} else static if(isIntegral!(Type)) {
65 		enum typeToTypeName = "Int";
66 	} else static if(isSomeString!Type) {
67 		enum typeToTypeName = "String";
68 	} else {
69 		enum typeToTypeName = Type.stringof;
70 	}
71 }
72 
73 unittest {
74 	import std.datetime : Date;
75 	static assert(typeToTypeName!(GQLDCustomLeaf!Date) == "Date");
76 }
77 
78 template typeToParameterTypeName(Type) {
79 	template level2(Type) {
80 		static if(is(Type : Nullable!F, F)) {
81 			enum level2 = typeToTypeName!F;
82 		} else {
83 			enum level2 = typeToTypeName!Type ~ "!";
84 		}
85 	}
86 
87 	template level1(Type) {
88 		static if(isArray!Type && !isSomeString!Type) {
89 			enum level1 = "[" ~ level2!(ElementEncodingType!Type) ~ "]";
90 		} else {
91 			enum level1 = typeToTypeName!Type;
92 		}
93 	}
94 
95 	template level0(Type) {
96 		static if(is(Type : Nullable!F, F)) {
97 			enum level0 = level1!F;
98 		} else {
99 			enum level0 = level1!Type ~ "!";
100 		}
101 	}
102 
103 	enum typeToParameterTypeName = level0!Type;
104 }
105 
106 unittest {
107 	static assert(typeToParameterTypeName!(int) == "Int!");
108 	static assert(typeToParameterTypeName!(Nullable!int) == "Int");
109 	static assert(typeToParameterTypeName!(double) == "Float!");
110 	static assert(typeToParameterTypeName!(Nullable!double) == "Float");
111 	static assert(typeToParameterTypeName!(double[]) == "[Float!]!");
112 	static assert(typeToParameterTypeName!(Nullable!(double)[]) == "[Float]!");
113 	static assert(typeToParameterTypeName!(Nullable!(Nullable!(double)[])) ==
114 			"[Float]");
115 }
116 
117 template isScalarType(Type) {
118 	static if(is(Type == bool)) {
119 		enum isScalarType = true;
120 	} else static if(is(Type == GQLDCustomLeaf!Fs, Fs...)) {
121 		enum isScalarType = true;
122 	} else static if(isFloatingPoint!(Type)) {
123 		enum isScalarType = true;
124 	} else static if(isIntegral!(Type)) {
125 		enum isScalarType = true;
126 	} else static if(isSomeString!Type) {
127 		enum isScalarType = true;
128 	} else static if(is(Type == enum)) {
129 		enum isScalarType = true;
130 	} else {
131 		enum isScalarType = false;
132 	}
133 }
134 
135 template typeToFieldType(Type) {
136 	static if(isArray!Type && !isSomeString!Type) {
137 		enum typeToFieldType = "__listType";
138 	} else static if(is(Type : Nullable!F, F)) {
139 		enum typeToFieldType = F.stringof;
140 	} else static if(is(Type : NullableStore!F, F)) {
141 		enum typeToFieldType = Type.TypeValue.stringof;
142 	} else {
143 		enum typeToFieldType = "__nonNullType";
144 	}
145 }
146 
147 Json typeFields(T)() {
148 	import graphql.uda;
149 	static enum memsToIgnore = ["__ctor", "toString", "toHash", "opCmp",
150 			"opEquals", "Monitor", "factory"];
151 	Json ret = Json.emptyArray();
152 	alias TplusParents = AliasSeq!(T, InheritedClasses!T);
153 	static foreach(Type; TplusParents) {{
154 		static foreach(mem; __traits(allMembers, Type)) {{
155 			enum GQLDUdaData udaData = getUdaData!(Type, mem);
156 			static if(!canFind(memsToIgnore, mem)
157 					&& udaData.ignore != Ignore.yes)
158 			{
159 				Json tmp = Json.emptyObject();
160 				tmp[Constants.name] = mem;
161 				tmp[Constants.__typename] = "__Field"; // needed for interfacesForType
162 				tmp[Constants.description] = udaData.description.text.empty
163 						? Json(null)
164 						: Json(udaData.description.text);
165 
166 				tmp[Constants.isDeprecated] =
167 					udaData.deprecationInfo.isDeprecated == IsDeprecated.yes
168 						? true
169 						: false;
170 
171 				tmp[Constants.deprecationReason] =
172 					udaData.deprecationInfo.isDeprecated == IsDeprecated.yes
173 						? Json(udaData.deprecationInfo.deprecationReason)
174 						: Json(null);
175 
176 				tmp[Constants.args] = Json.emptyArray();
177 				static if(isCallable!(__traits(getMember, Type, mem))) {
178 					alias RT = ReturnType!(__traits(getMember, Type, mem));
179 					alias RTS = stripArrayAndNullable!RT;
180 					tmp[Constants.typenameOrig] = typeToTypeName!(RT);
181 
182 					// InputValue
183 					alias paraNames = ParameterIdentifierTuple!(
184 							__traits(getMember, Type, mem)
185 						);
186 					alias paraTypes = Parameters!(
187 							__traits(getMember, Type, mem)
188 						);
189 					alias paraDefs = ParameterDefaults!(
190 							__traits(getMember, Type, mem)
191 						);
192 					static foreach(idx; 0 .. paraNames.length) {{
193 						Json iv = Json.emptyObject();
194 						iv[Constants.name] = paraNames[idx];
195 						// needed for interfacesForType
196 						iv[Constants.__typename] = Constants.__InputValue;
197 						iv[Constants.description] = Json(null);
198 						iv[Constants.typenameOrig] =
199 							typeToParameterTypeName!(paraTypes[idx]);
200 						static if(!is(paraDefs[idx] == void)) {
201 							iv[Constants.defaultValue] = serializeToJson(paraDefs[idx])
202 								.toString();
203 						} else {
204 							iv[Constants.defaultValue] = Json(null);
205 						}
206 						tmp[Constants.args] ~= iv;
207 					}}
208 				} else {
209 					tmp[Constants.typenameOrig] = typeToTypeName!(
210 							typeof(__traits(getMember, Type, mem))
211 						);
212 				}
213 				ret ~= tmp;
214 			}
215 		}}
216 	}}
217 	return ret;
218 }
219 
220 Json inputFields(Type)() {
221 	Json ret = Json.emptyArray();
222 	alias types = FieldTypeTuple!Type;
223 	alias names = FieldNameTuple!Type;
224 	static foreach(idx; 0 .. types.length) {{
225 		enum GQLDUdaData udaData = getUdaData!(types[idx]);
226 		Json tmp = Json.emptyObject();
227 		tmp[Constants.name] = names[idx];
228 		tmp[Constants.description] = udaData.description.text.empty
229 				? Json(null)
230 				: Json(udaData.description.text);
231 
232 		// needed for interfacesForType
233 		tmp[Constants.__typename] = Constants.__InputValue;
234 
235 		tmp[Constants.typenameOrig] = typeToTypeName!(types[idx]);
236 		tmp[Constants.defaultValue] = serializeToJson(
237 				__traits(getMember, Type.init, names[idx])
238 			);
239 		ret ~= tmp;
240 	}}
241 	return ret;
242 }
243 
244 Json emptyType() {
245 	Json ret = Json.emptyObject();
246 	ret[Constants.name] = Json(null);
247 	ret[Constants.description] = Json(null);
248 	ret[Constants.fields] = Json(null);
249 	ret[Constants.interfacesNames] = Json(null);
250 	ret[Constants.possibleTypesNames] = Json(null);
251 	ret[Constants.enumValues] = Json(null);
252 	ret["ofType"] = Json(null);
253 	return ret;
254 }
255 
256 Json removeNonNullAndList(Json j) {
257 	string t = j["kind"].get!string();
258 	if(t == "NON_NULL" || t == "LIST") {
259 		return removeNonNullAndList(j["ofType"]);
260 	} else {
261 		return j;
262 	}
263 }
264 
265 // remove the top nullable to find out if we have a NON_NULL or not
266 Json typeToJson(Type,Schema)() {
267 	static if(is(Type : Nullable!F, F)) {
268 		return typeToJson1!(F,Schema,Type)();
269 	} else static if(is(Type : NullableStore!F, F)) {
270 		return typeToJson1!(Type.TypeValue,Schema,Type)();
271 	} else {
272 		Json ret = emptyType();
273 		ret["kind"] = "NON_NULL";
274 		ret[Constants.__typename] = "__Type";
275 		ret["ofType"] = typeToJson1!(Type,Schema,Type)();
276 		return ret;
277 	}
278 }
279 
280 // remove the array is present
281 Json typeToJson1(Type,Schema,Orig)() {
282 	static if(isArray!Type && !isSomeString!Type && !is(Type == enum)) {
283 		Json ret = emptyType();
284 		ret["kind"] = "LIST";
285 		ret[Constants.__typename] = "__Type";
286 		ret["ofType"] = typeToJson2!(ElementEncodingType!Type, Schema, Orig)();
287 		return ret;
288 	} else {
289 		return typeToJsonImpl!(Type, Schema, Orig)();
290 	}
291 }
292 
293 // remove another nullable
294 Json typeToJson2(Type,Schema,Orig)() {
295 	static if(is(Type : Nullable!F, F)) {
296 		return typeToJsonImpl!(F, Schema, Orig)();
297 	} else static if(is(Type : NullableStore!F, F)) {
298 		return typeToJsonImpl!(Type.TypeValue, Schema, Orig)();
299 	} else {
300 		Json ret = emptyType();
301 		ret["kind"] = "NON_NULL";
302 		ret[Constants.__typename] = "__Type";
303 		ret["ofType"] = typeToJsonImpl!(Type, Schema, Orig)();
304 		return ret;
305 	}
306 }
307 
308 Json typeToJsonImpl(Type,Schema,Orig)() {
309 	Json ret = Json.emptyObject();
310 	enum string kind = typeToTypeEnum!Type;
311 	ret["kind"] = kind;
312 	ret[Constants.__typename] = "__Type";
313 	ret[Constants.name] = typeToTypeName!Type;
314 
315 	enum GQLDUdaData udaData = getUdaData!(Orig);
316 	ret[Constants.description] = udaData.description.text.empty
317 			? Json(null)
318 			: Json(udaData.description.text);
319 
320 	ret[Constants.isDeprecated] =
321 		udaData.deprecationInfo.isDeprecated == IsDeprecated.yes
322 			? true
323 			: false;
324 
325 	ret[Constants.deprecationReason] =
326 		udaData.deprecationInfo.isDeprecated == IsDeprecated.yes
327 			? Json(udaData.deprecationInfo.deprecationReason)
328 			: Json(null);
329 
330 	// fields
331 	static if((is(Type == class) || is(Type == interface) || is(Type == struct))
332 			&& !is(Type : Nullable!K, K) && !is(Type : NullableStore!K, K)
333 			&& !is(Type : GQLDCustomLeaf!Ks, Ks...))
334 	{
335 		ret[Constants.fields] = typeFields!Type();
336 	} else {
337 		ret[Constants.fields] = Json(null);
338 	}
339 
340 	// inputFields
341 	static if(kind == Constants.INPUT_OBJECT) {
342 		ret[Constants.inputFields] = inputFields!Type();
343 	} else {
344 		ret[Constants.inputFields] = Json(null);
345 	}
346 
347 	// needed to resolve interfaces
348 	static if(is(Type == class) || is(Type == interface)) {
349 		ret[Constants.interfacesNames] = Json.emptyArray();
350 		static foreach(interfaces; InheritedClasses!Type) {{
351 			ret[Constants.interfacesNames] ~= interfaces.stringof;
352 		}}
353 	} else {
354 		ret[Constants.interfacesNames] = Json(null);
355 	}
356 
357 	// needed to resolve possibleTypes
358 	static if(is(Type == class) || is(Type == union)
359 			|| is(Type == interface))
360 	{
361 		ret[Constants.possibleTypesNames] = Json.emptyArray();
362 		alias PT = PossibleTypes!(Type, Schema);
363 		static foreach(pt; PT) {
364 			ret[Constants.possibleTypesNames] ~= pt.stringof;
365 		}
366 	} else {
367 		ret[Constants.possibleTypesNames] = Json(null);
368 	}
369 
370 	// enumValues
371 	static if(is(Type == enum)) {
372 		ret[Constants.enumValues] = Json.emptyArray();
373 		static foreach(mem; EnumMembers!Type) {{
374 			Json tmp = Json.emptyObject();
375 			tmp[Constants.__TypeKind] = Constants.__EnumValue;
376 			tmp[Constants.name] = Json(to!string(mem));
377 			tmp[Constants.description] = "ENUM_DESCRIPTION_TODO";
378 			tmp[Constants.isDeprecated] = false;
379 			tmp[Constants.deprecationReason] = "ENUM_DEPRECATIONREASON_TODO";
380 			ret[Constants.enumValues] ~= tmp;
381 		}}
382 	} else {
383 		ret[Constants.enumValues] = Json(null);
384 	}
385 
386 	// needed to resolve ofType
387 	static if(is(Type : Nullable!F, F) || is(Type : NullableStore!F, F)
388 			|| is(Type : GQLDCustomLeaf!Fs, Fs...))
389 	{
390 		ret[Constants.ofTypeName] = Fs[0].stringof;
391 	} else static if(isArray!Type) {
392 		ret[Constants.ofTypeName] = ElementEncodingType!(Type).stringof;
393 	}
394 
395 	return ret;
396 }
397 
398 Json directivesToJson(Directives)() {
399 	import std.string : stripLeft;
400 	Json ret = Json.emptyArray();
401 	static enum memsToIgnore = ["__ctor", "toString", "toHash", "opCmp",
402 			"opEquals", "Monitor", "factory"];
403 	alias TplusParents = AliasSeq!(Directives, InheritedClasses!Directives);
404 	static foreach(Type; TplusParents) {{
405 		static foreach(mem; __traits(allMembers, Type)) {{
406 			static if(!canFind(memsToIgnore, mem)) {
407 				Json tmp = Json.emptyObject();
408 				enum GQLDUdaData udaData = getUdaData!(Type, mem);
409 				tmp[Constants.name] = mem;
410 				// needed for interfacesForType
411 				tmp[Constants.__typename] = Constants.__Directive;
412 				tmp[Constants.description] = udaData.description.text.empty
413 						? Json(null)
414 						: Json(udaData.description.text);
415 
416 				tmp[Constants.isDeprecated] =
417 					udaData.deprecationInfo.isDeprecated == IsDeprecated.yes
418 						? true
419 						: false;
420 
421 				tmp[Constants.deprecationReason] =
422 					udaData.deprecationInfo.isDeprecated == IsDeprecated.yes
423 						? Json(udaData.deprecationInfo.deprecationReason)
424 						: Json(null);
425 
426 				//tmp[Constants.description] = Json(null);
427 				tmp[Constants.locations] = Json.emptyArray();
428 				tmp[Constants.args] = Json.emptyArray();
429 				static if(isCallable!(__traits(getMember, Type, mem))) {
430 					// InputValue
431 					alias paraNames = ParameterIdentifierTuple!(
432 							__traits(getMember, Type, mem)
433 						);
434 					alias paraTypes = Parameters!(
435 							__traits(getMember, Type, mem)
436 						);
437 					alias paraDefs = ParameterDefaults!(
438 							__traits(getMember, Type, mem)
439 						);
440 					static foreach(idx; 0 .. paraNames.length) {{
441 						Json iv = Json.emptyObject();
442 						// TODO remove the strip left. Its in because the
443 						// two default directives of GraphQL skip and include
444 						// both have one parameter named "if".
445 						iv[Constants.name] = stripLeft(paraNames[idx], "_");
446 						iv[Constants.description] = Json(null);
447 						// needed for interfacesForType
448 						iv[Constants.__typename] = Constants.__InputValue;
449 						iv[Constants.typenameOrig] = typeToTypeName!(paraTypes[idx]);
450 						static if(!is(paraDefs[idx] == void)) {
451 							iv[Constants.defaultValue] = serializeToJson(paraDefs[idx])
452 								.toString();
453 						} else {
454 							iv[Constants.defaultValue] = Json(null);
455 						}
456 						tmp[Constants.args] ~= iv;
457 					}}
458 				}
459 				ret ~= tmp;
460 			}
461 		}}
462 	}}
463 	return ret;
464 }
465 
466 Json getField(Json j, string name) {
467 	import graphql.constants;
468 
469 	if(j.type != Json.Type.object || Constants.fields !in j
470 			|| j[Constants.fields].type != Json.Type.array)
471 	{
472 		return Json.init;
473 	}
474 
475 	foreach(it; j[Constants.fields].byValue) {
476 		string itName = it[Constants.name].get!string();
477 		if(itName == name) {
478 			return it;
479 		}
480 	}
481 	return Json.init;
482 }
483 
484 Json getIntrospectionField(string name) {
485 	import std.format : format;
486 	Json ret = Json.emptyObject();
487 	ret[Constants.typenameOrig] = name == Constants.__typename
488 		? "String"
489 		: name == Constants.__schema
490 			? "__Schema"
491 			: name == Constants.__type
492 				? "__Type"
493 				: format("Not known introspection name '%s'", name);
494 	return ret;
495 }