1 module graphql.helper;
2 
3 import std.algorithm.searching : canFind;
4 import std.algorithm.iteration : splitter;
5 import std.format : format;
6 import std.exception : enforce, assertThrown;
7 import std.experimental.logger;
8 import std.datetime : DateTime;
9 
10 import vibe.data.json;
11 
12 import graphql.ast;
13 import graphql.constants;
14 
15 @safe:
16 
17 enum d = "data";
18 enum e = Constants.errors;
19 
20 string firstCharUpperCase(string input) {
21 	import std.conv : to;
22 	import std.uni : isUpper, toUpper;
23 	import std.array : front, popFront;
24 	if(isUpper(input.front)) {
25 		return input;
26 	}
27 
28 	const f = input.front;
29 	input.popFront();
30 
31 	return to!string(toUpper(f)) ~ input;
32 }
33 
34 Json returnTemplate() {
35 	Json ret = Json.emptyObject();
36 	ret["data"] = Json.emptyObject();
37 	ret[Constants.errors] = Json.emptyArray();
38 	return ret;
39 }
40 
41 void insertError(T)(ref Json result, T t) {
42 	Json tmp = Json.emptyObject();
43 	tmp["message"] = serializeToJson(t);
44 	if(e !in result) {
45 		result[e] = Json.emptyArray();
46 	}
47 	enforce(result[e].type == Json.Type.array);
48 	result[e] ~= tmp;
49 }
50 
51 void insertPayload(ref Json result, string field, Json data) {
52 	if(d in data) {
53 		if(d !in result) {
54 			result[d] = Json.emptyObject();
55 		}
56 		enforce(result[d].type == Json.Type.object);
57 		Json* df = field in result[d];
58 		if(df) {
59 			result[d][field] = joinJson(*df, data[d]);
60 		} else {
61 			result[d][field] = data[d];
62 		}
63 	}
64 	if(e in data) {
65 		if(e !in result) {
66 			result[e] = Json.emptyArray();
67 		}
68 		enforce(result[e].type == Json.Type.array);
69 		if(!canFind(result[e].byValue(), data[e])) {
70 			result[e] ~= data[e];
71 		}
72 	}
73 }
74 
75 unittest {
76 	Json old = returnTemplate();
77 	old["data"]["foo"] = Json.emptyObject();
78 	old["data"]["foo"]["a"] = 1337;
79 
80 	Json n = returnTemplate();
81 	n["data"] = Json.emptyObject();
82 	n["data"]["b"] = 1338;
83 
84 	old.insertPayload("foo", n);
85 	assert(old["data"]["foo"].length == 2, format("%s %s",
86 			old["data"]["foo"].length, old.toPrettyString())
87 		);
88 	assert("a" in old["data"]["foo"]);
89 	assert("b" in old["data"]["foo"]);
90 }
91 
92 bool isScalar(ref const(Json) data) {
93 	return data.type == Json.Type.bigInt
94 			|| data.type == Json.Type.bool_
95 			|| data.type == Json.Type.float_
96 			|| data.type == Json.Type.int_
97 			|| data.type == Json.Type..string;
98 }
99 
100 bool dataIsEmpty(ref const(Json) data) {
101 	import std.experimental.logger;
102 	if(data.type == Json.Type.object) {
103 		foreach(key, value; data.byKeyValue()) {
104 			if(key != Constants.errors && !value.dataIsEmpty()) {
105 			//if(key != Constants.errors) { // Issue #22 place to look at
106 				return false;
107 			}
108 		}
109 		return true;
110 	} else if(data.type == Json.Type.null_
111 			|| data.type == Json.Type.undefined
112 		)
113 	{
114 		return true;
115 	} else if(data.type == Json.Type.array) {
116 		return data.length == 0;
117 	} else if(data.type == Json.Type.bigInt
118 			|| data.type == Json.Type.bool_
119 			|| data.type == Json.Type.float_
120 			|| data.type == Json.Type.int_
121 			|| data.type == Json.Type..string
122 		)
123 	{
124 		return false;
125 	}
126 
127 	return true;
128 }
129 
130 unittest {
131 	string t = `{ "errors" : {} }`;
132 	Json j = parseJsonString(t);
133 	assert(j.dataIsEmpty());
134 }
135 
136 unittest {
137 	string t = `{ "kind": {}, "fields": null, "name": {} }`;
138 	Json j = parseJsonString(t);
139 	//assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22
140 	assert(j.dataIsEmpty());
141 }
142 
143 unittest {
144 	string t =
145 `{
146 	"name" : {
147 		"foo" : null
148 	}
149 }`;
150 	Json j = parseJsonString(t);
151 	//assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22
152 	assert(j.dataIsEmpty());
153 }
154 
155 bool dataIsNull(ref const(Json) data) {
156 	import std.format : format;
157 	enforce(data.type == Json.Type.object, format("%s", data));
158 	if(const(Json)* d = "data" in data) {
159 		return d.type == Json.Type.null_;
160 	}
161 	return false;
162 }
163 
164 Json getWithPath(Json input, string path) {
165 	auto sp = path.splitter(".");
166 	foreach(s; sp) {
167 		Json* n = s in input;
168 		enforce(n !is null, "failed to traverse the input at " ~ s);
169 		input = *n;
170 	}
171 	return input;
172 }
173 
174 unittest {
175 	string t =
176 `{
177 	"name" : {
178 		"foo" : 13
179 	}
180 }`;
181 	Json j = parseJsonString(t);
182 	Json f = j.getWithPath("name");
183 	assert("foo" in f);
184 
185 	f = j.getWithPath("name.foo");
186 	enforce(f.to!int() == 13);
187 
188 	assertThrown(j.getWithPath("doesnotexist"));
189 	assertThrown(j.getWithPath("name.alsoNotThere"));
190 }
191 
192 enum JoinJsonPrecedence {
193 	none,
194 	a,
195 	b
196 }
197 
198 /** Merge two Json objects.
199 Values in a take precedence over values in b.
200 */
201 Json joinJson(JoinJsonPrecedence jjp = JoinJsonPrecedence.none)(Json a, Json b)
202 {
203 	// we can not merge null or undefined values
204 	if(a.type == Json.Type.null_ || a.type == Json.Type.undefined) {
205 		return b;
206 	}
207 	if(b.type == Json.Type.null_ || b.type == Json.Type.undefined) {
208 		return a;
209 	}
210 
211 	// we need objects to merge
212 	if(a.type == Json.Type.object && b.type == Json.Type.object) {
213 		Json ret = a.clone();
214 		foreach(key, value; b.byKeyValue()) {
215 			Json* ap = key in ret;
216 			if(ap is null) {
217 				ret[key] = value;
218 			} else if(ap.type == Json.Type.object
219 					&& value.type == Json.Type.object)
220 			{
221 				ret[key] = joinJson(*ap, value);
222 			} else {
223 				static if(jjp == JoinJsonPrecedence.none) {
224 					throw new Exception(format(
225 							"Can not join '%s' and '%s' on key '%s'",
226 							ap.type, value.type, key));
227 				} else static if(jjp == JoinJsonPrecedence.a) {
228 				} else {
229 					ret[key] = value;
230 				}
231 			}
232 		}
233 		return ret;
234 	}
235 	return a;
236 }
237 
238 unittest {
239 	Json a = parseJsonString(`{"overSize":200}`);
240 	Json b = parseJsonString(`{}`);
241 	const c = joinJson(b, a);
242 	assert(c == a);
243 
244 	b = parseJsonString(`{"underSize":-100}`);
245 	const d = joinJson(b, a);
246 	Json r = parseJsonString(`{"overSize":200, "underSize":-100}`);
247 	assert(d == r);
248 }
249 
250 unittest {
251 	Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
252 			parseJsonString(`{"underSize": {"b": 100}}`)
253 		);
254 
255 	Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`);
256 	assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString()));
257 }
258 
259 unittest {
260 	assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
261 			parseJsonString(`{"underSize": {"a": 100}}`)
262 		));
263 }
264 
265 unittest {
266 	assertThrown(joinJson(parseJsonString(`{"underSize": -100}`),
267 			parseJsonString(`{"underSize": {"a": 100}}`)
268 		));
269 }
270 
271 template toType(T) {
272 	import std.bigint : BigInt;
273 	import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint,
274 		   isSomeString;
275 	static if(is(T == bool)) {
276 		enum toType = Json.Type.bool_;
277 	} else static if(isIntegral!(T)) {
278 		enum toType = Json.Type.int_;
279 	} else static if(isFloatingPoint!(T)) {
280 		enum toType = Json.Type.float_;
281 	} else static if(isSomeString!(T)) {
282 		enum toType = Json.Type..string;
283 	} else static if(isArray!(T)) {
284 		enum toType = Json.Type.array;
285 	} else static if(isAggregateType!(T)) {
286 		enum toType = Json.Type.object;
287 	} else static if(is(T == BigInt)) {
288 		enum toType = Json.Type.bigint;
289 	} else {
290 		enum toType = Json.Type.undefined;
291 	}
292 }
293 
294 bool hasPathTo(T)(Json data, string path, ref T ret) {
295 	enum TT = toType!T;
296 	auto sp = path.splitter(".");
297 	string f;
298 	while(!sp.empty) {
299 		f = sp.front;
300 		sp.popFront();
301 		if(data.type != Json.Type.object || f !in data) {
302 			return false;
303 		} else {
304 			data = data[f];
305 		}
306 	}
307 	static if(is(T == Json)) {
308 		ret = data;
309 		return true;
310 	} else {
311 		if(data.type == TT) {
312 			ret = data.to!T();
313 			return true;
314 		}
315 		return false;
316 	}
317 }
318 
319 unittest {
320 	Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`);
321 	Json ret;
322 	assert(hasPathTo!Json(d, "foo", ret));
323 }
324 
325 /**
326 params:
327 	path = A "." seperated path
328 */
329 T getWithDefault(T)(Json data, string[] paths...) {
330 	enum TT = toType!T;
331 	T ret = T.init;
332 	foreach(string path; paths) {
333 		if(hasPathTo!T(data, path, ret)) {
334 			return ret;
335 		}
336 	}
337 	return ret;
338 }
339 
340 unittest {
341 	Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8,
342 			"__typename":"Starship","series":["DeepSpaceNine",
343 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
344 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`);
345 	const r = d.getWithDefault!string("data.__typename");
346 	assert(r == "Starship", r);
347 }
348 
349 unittest {
350 	Json d = parseJsonString(`{"commanderId":8,
351 			"__typename":"Starship","series":["DeepSpaceNine",
352 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
353 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
354 	const r = d.getWithDefault!string("data.__typename", "__typename");
355 	assert(r == "Starship", r);
356 }
357 
358 unittest {
359 	Json d = parseJsonString(`{"commanderId":8,
360 			"__typename":"Starship","series":["DeepSpaceNine",
361 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
362 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
363 	const r = d.getWithDefault!string("__typename");
364 	assert(r == "Starship", r);
365 }
366 
367 // TODO should return ref
368 auto accessNN(string[] tokens,T)(T tmp0) {
369 	import std.array : back;
370 	import std.format : format;
371 	if(tmp0 !is null) {
372 		static foreach(idx, token; tokens) {
373 			mixin(format!
374 				`if(tmp%d is null) return null;
375 				auto tmp%d = tmp%d.%s;`(idx, idx+1, idx, token)
376 			);
377 		}
378 		return mixin(format("tmp%d", tokens.length));
379 	}
380 	return null;
381 }
382 
383 unittest {
384 	class A {
385 		int a;
386 	}
387 
388 	class B {
389 		A a;
390 	}
391 
392 	class C {
393 		B b;
394 	}
395 
396 	auto c1 = new C;
397 	assert(c1.accessNN!(["b", "a"]) is null);
398 
399 	c1.b = new B;
400 	assert(c1.accessNN!(["b"]) !is null);
401 
402 	assert(c1.accessNN!(["b", "a"]) is null);
403 	// TODO not sure why this is not a lvalue
404 	//c1.accessNN!(["b", "a"]) = new A;
405 	c1.b.a = new A;
406 	assert(c1.accessNN!(["b", "a"]) !is null);
407 }
408 
409 T extract(T)(Json data, string name) {
410 	enforce(data.type == Json.Type.object, format!
411 			"Trying to get a '%s' by name '%s' but passed Json is not an object"
412 			(T.stringof, name)
413 		);
414 
415 	Json* item = name in data;
416 
417 	enforce(item !is null, format!(
418 			"Trying to get a '%s' by name '%s' which is not present in passed "
419 			~ "object '%s'"
420 			)(T.stringof, name, data)
421 		);
422 
423 	return (*item).to!T();
424 }
425 
426 unittest {
427 	import std.exception : assertThrown;
428 	Json j = parseJsonString(`{ "foo": 1337 }`);
429 	auto foo = j.extract!int("foo");
430 
431 	assertThrown(Json.emptyObject().extract!float("Hello"));
432 	assertThrown(j.extract!string("Hello"));
433 }
434 
435 const(Document) lexAndParse(string s) {
436 	import graphql.lexer;
437 	import graphql.parser;
438 	auto l = Lexer(s, QueryParser.no);
439 	auto p = Parser(l);
440 	const(Document) doc = p.parseDocument();
441 	return doc;
442 }
443 
444 string stringTypeStrip(string str) {
445 	import std.algorithm.searching : startsWith, endsWith, canFind;
446 	import std.string : capitalize, indexOf, strip;
447 	immutable fs = ["Nullable!", "NullableStore!", "GQLDCustomLeaf!"];
448 	immutable arr = "[]";
449 	outer: while(true) {
450 		str = str.strip();
451 		foreach(f; fs) {
452 			if(str.startsWith(f)) {
453 				str = str[f.length .. $];
454 				continue outer;
455 			}
456 		}
457 		if(str.endsWith(arr)) {
458 			str = str[0 .. str.length - arr.length];
459 			continue;
460 		} else if(str.endsWith("!")) {
461 			str = str[0 .. $ - 1];
462 			continue;
463 		} else if(str.startsWith("[")) {
464 			str = str[1 .. $];
465 			continue;
466 		} else if(str.endsWith("]")) {
467 			str = str[0 .. $ - 1];
468 			continue;
469 		} else if(str.startsWith("(")) {
470 			str = str[1 .. $];
471 			continue;
472 		} else if(str.endsWith(")")) {
473 			str = str[0 .. $ - 1];
474 			continue;
475 		} else if(str.endsWith("'")) {
476 			str = str[0 .. $ - 1];
477 			continue;
478 		}
479 		break;
480 	}
481 
482 	const comma = str.indexOf(',');
483 	str = comma == -1 ? str : str[0 .. comma].strip();
484 
485 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
486 		? "Int"
487 		: str;
488 
489 	str = canFind(["string", "int", "float", "bool"], str)
490 		? capitalize(str)
491 		: str;
492 
493 	str = str == "__type" ? "__Type" : str;
494 	str = str == "__schema" ? "__Schema" : str;
495 	str = str == "__inputvalue" ? "__InputValue" : str;
496 	str = str == "__directive" ? "__Directive" : str;
497 	str = str == "__field" ? "__Field" : str;
498 
499 	return str;
500 }
501 
502 unittest {
503 	string t = "Nullable!string";
504 	string r = t.stringTypeStrip();
505 	assert(r == "String", r);
506 
507 	t = "Nullable!(string[])";
508 	r = t.stringTypeStrip();
509 	assert(r == "String", r);
510 }
511 
512 unittest {
513 	string t = "Nullable!__type";
514 	string r = t.stringTypeStrip();
515 	assert(r == "__Type", r);
516 
517 	t = "Nullable!(__type[])";
518 	r = t.stringTypeStrip();
519 	assert(r == "__Type", r);
520 }
521 
522 Json toGraphqlJson(T)(auto ref T input) {
523 	import std.typecons : Nullable;
524 	import std.array : empty;
525 	import std.traits : isArray, isAggregateType, isBasicType, isSomeString,
526 		   isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple;
527 
528 	import nullablestore;
529 
530 	import graphql.uda : GQLDCustomLeaf;
531 	static if(isArray!T && !isSomeString!T) {
532 		Json ret = Json.emptyArray();
533 		foreach(ref it; input) {
534 			ret ~= toGraphqlJson(it);
535 		}
536 		return ret;
537 	} else static if(is(T : GQLDCustomLeaf!Type, Type...)) {
538 		return Json(Type[1](input));
539 	} else static if(is(T : Nullable!Type, Type)) {
540 		return input.isNull() ? Json(null) : toGraphqlJson(input.get());
541 	} else static if(isBasicType!T || isScalarType!T || isSomeString!T) {
542 		return serializeToJson(input);
543 	} else static if(isAggregateType!T) {
544 		Json ret = Json.emptyObject();
545 
546 		// the important bit is the setting of the __typename field
547 		ret["__typename"] = T.stringof;
548 		alias names = FieldNameTuple!(T);
549 		alias types = FieldTypeTuple!(T);
550 		static foreach(idx; 0 .. names.length) {{
551 			static if(!names[idx].empty) {
552 				static if(is(types[idx] : NullableStore!Type, Type)) {
553 				} else static if(is(types[idx] == enum)) {
554 					ret[names[idx]] = serializeToJson(
555 							__traits(getMember, input, names[idx])
556 						);
557 				} else {
558 					ret[names[idx]] = toGraphqlJson(
559 							__traits(getMember, input, names[idx])
560 						);
561 				}
562 			}
563 		}}
564 		return ret;
565 	} else {
566 		static assert(false, T.stringof ~ " not supported");
567 	}
568 }
569 
570 /*Json toGraphqlJson(T)(auto ref T obj) {
571 	import graphql.uda : GQLDCustomLeaf;
572 	import std.traits : isArray, isSomeString, FieldNameTuple, FieldTypeTuple;
573 	import std.array : empty;
574 	import std.typecons : Nullable;
575 	import nullablestore;
576 
577 	alias names = FieldNameTuple!(T);
578 	alias types = FieldTypeTuple!(T);
579 
580 	static if(isArray!T && !isSomeString!T) {
581 		Json ret = Json.emptyArray();
582 		foreach(ref it; obj) {
583 			ret ~= toGraphqlJson(it);
584 		}
585 	} else static if(is(T : GQLDCustomLeaf!Type, Type...)) {
586 		Json ret = Json(Type[1](obj));
587 	} else {
588 		Json ret = Json.emptyObject();
589 
590 		// the important bit is the setting of the __typename field
591 		ret["__typename"] = T.stringof;
592 
593 		static foreach(idx; 0 .. names.length) {{
594 			static if(!names[idx].empty) {
595 				//writefln("%s %s", __LINE__, names[idx]);
596 				static if(is(types[idx] : Nullable!Type, Type)) {
597 					if(__traits(getMember, obj, names[idx]).isNull()) {
598 						ret[names[idx]] = Json(null);
599 					} else {
600 						ret[names[idx]] = toGraphqlJson(
601 								__traits(getMember, obj, names[idx]).get()
602 							);
603 					}
604 				} else static if(is(types[idx] : GQLDCustomLeaf!Type, Type...)) {
605 					ret[names[idx]] = Json(Type[1](
606 							__traits(getMember, obj, names[idx]))
607 						);
608 				} else static if(is(types[idx] : NullableStore!Type, Type)) {
609 				} else {
610 					ret[names[idx]] = serializeToJson(
611 							__traits(getMember, obj, names[idx])
612 						);
613 				}
614 			}
615 		}}
616 	}
617 	return ret;
618 }*/
619 
620 string dtToString(DateTime dt) {
621 	return dt.toISOExtString();
622 }
623 
624 unittest {
625 	import std.typecons : nullable, Nullable;
626 	import graphql.uda;
627 	import nullablestore;
628 
629 	struct Foo {
630 		int a;
631 		Nullable!int b;
632 		NullableStore!float c;
633 		GQLDCustomLeaf!(DateTime, dtToString) dt2;
634 		Nullable!(GQLDCustomLeaf!(DateTime, dtToString)) dt;
635 	}
636 
637 	DateTime dt = DateTime(1337, 7, 1, 1, 1, 1);
638 	DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3);
639 
640 	Foo foo;
641 	foo.dt2 = GQLDCustomLeaf!(DateTime, dtToString)(dt2);
642 	foo.dt = nullable(GQLDCustomLeaf!(DateTime, dtToString)(dt));
643 	Json j = toGraphqlJson(foo);
644 	assert(j["a"].to!int() == 0);
645 	assert(j["b"].type == Json.Type.null_);
646 	assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type,
647 				j.toPrettyString()
648 			)
649 		);
650 	string exp = j["dt"].to!string();
651 	assert(exp == "1337-07-01T01:01:01", exp);
652 	string exp2 = j["dt2"].to!string();
653 	assert(exp2 == "2337-07-01T01:01:03", exp2);
654 }