1 module graphql.helper;
2 
3 import std.array : empty;
4 import std.algorithm.iteration : each, splitter;
5 import std.algorithm.searching : startsWith, endsWith, canFind;
6 import std.conv : to;
7 import std.datetime : DateTime, Date;
8 import std.exception : enforce, assertThrown;
9 import std.experimental.logger;
10 import std.format : format;
11 import std.stdio;
12 import std.string : capitalize, indexOf, strip;
13 import std.typecons : nullable, Nullable;
14 
15 import vibe.data.json;
16 
17 import graphql.ast;
18 import graphql.constants;
19 
20 @safe:
21 
22 enum d = "data";
23 enum e = Constants.errors;
24 
25 string firstCharUpperCase(string input) {
26 	import std.conv : to;
27 	import std.uni : isUpper, toUpper;
28 	import std.array : front, popFront;
29 	if(isUpper(input.front)) {
30 		return input;
31 	}
32 
33 	const f = input.front;
34 	input.popFront();
35 
36 	return to!string(toUpper(f)) ~ input;
37 }
38 
39 Json returnTemplate() {
40 	Json ret = Json.emptyObject();
41 	ret["data"] = Json.emptyObject();
42 	ret[Constants.errors] = Json.emptyArray();
43 	return ret;
44 }
45 
46 void insertError(T)(ref Json result, T t) {
47 	insertError(result, t, []);
48 }
49 
50 void insertError(T)(ref Json result, T t, PathElement[] path) {
51 	Json tmp = Json.emptyObject();
52 	tmp["message"] = serializeToJson(t);
53 	if(!path.empty) {
54 		tmp["path"] = Json.emptyArray();
55 		path.each!(it => tmp["path"] ~= it.toJson());
56 	}
57 	if(e !in result) {
58 		result[e] = Json.emptyArray();
59 	}
60 	enforce(result[e].type == Json.Type.array);
61 	result[e] ~= tmp;
62 }
63 
64 void insertPayload(ref Json result, string field, Json data) {
65 	if(d in data) {
66 		if(d !in result) {
67 			result[d] = Json.emptyObject();
68 		}
69 		enforce(result[d].type == Json.Type.object);
70 		Json* df = field in result[d];
71 		if(df) {
72 			result[d][field] = joinJson(*df, data[d]);
73 		} else {
74 			result[d][field] = data[d];
75 		}
76 	}
77 	if(e in data) {
78 		if(e !in result) {
79 			result[e] = Json.emptyArray();
80 		}
81 		enforce(result[e].type == Json.Type.array);
82 		if(!canFind(result[e].byValue(), data[e])) {
83 			result[e] ~= data[e];
84 		}
85 	}
86 }
87 
88 unittest {
89 	Json old = returnTemplate();
90 	old["data"]["foo"] = Json.emptyObject();
91 	old["data"]["foo"]["a"] = 1337;
92 
93 	Json n = returnTemplate();
94 	n["data"] = Json.emptyObject();
95 	n["data"]["b"] = 1338;
96 
97 	old.insertPayload("foo", n);
98 	assert(old["data"]["foo"].length == 2, format("%s %s",
99 			old["data"]["foo"].length, old.toPrettyString())
100 		);
101 	assert("a" in old["data"]["foo"]);
102 	assert("b" in old["data"]["foo"]);
103 }
104 
105 bool isScalar(ref const(Json) data) {
106 	return data.type == Json.Type.bigInt
107 			|| data.type == Json.Type.bool_
108 			|| data.type == Json.Type.float_
109 			|| data.type == Json.Type.int_
110 			|| data.type == Json.Type..string;
111 }
112 
113 bool dataIsEmpty(ref const(Json) data) {
114 	import std.experimental.logger;
115 	if(data.type == Json.Type.object) {
116 		foreach(key, value; data.byKeyValue()) {
117 			if(key != Constants.errors && !value.dataIsEmpty()) {
118 			//if(key != Constants.errors) { // Issue #22 place to look at
119 				return false;
120 			}
121 		}
122 		return true;
123 	} else if(data.type == Json.Type.null_
124 			|| data.type == Json.Type.undefined
125 		)
126 	{
127 		return true;
128 	} else if(data.type == Json.Type.array) {
129 		return data.length == 0;
130 	} else if(data.type == Json.Type.bigInt
131 			|| data.type == Json.Type.bool_
132 			|| data.type == Json.Type.float_
133 			|| data.type == Json.Type.int_
134 			|| data.type == Json.Type..string
135 		)
136 	{
137 		return false;
138 	}
139 
140 	return true;
141 }
142 
143 unittest {
144 	string t = `{ "errors" : {} }`;
145 	Json j = parseJsonString(t);
146 	assert(j.dataIsEmpty());
147 }
148 
149 unittest {
150 	string t = `{ "kind": {}, "fields": null, "name": {} }`;
151 	Json j = parseJsonString(t);
152 	//assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22
153 	assert(j.dataIsEmpty());
154 }
155 
156 unittest {
157 	string t =
158 `{
159 	"name" : {
160 		"foo" : null
161 	}
162 }`;
163 	Json j = parseJsonString(t);
164 	//assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22
165 	assert(j.dataIsEmpty());
166 }
167 
168 bool dataIsNull(ref const(Json) data) {
169 	import std.format : format;
170 	enforce(data.type == Json.Type.object, format("%s", data));
171 	if(const(Json)* d = "data" in data) {
172 		return d.type == Json.Type.null_;
173 	}
174 	return false;
175 }
176 
177 Json getWithPath(Json input, string path) {
178 	auto sp = path.splitter(".");
179 	foreach(s; sp) {
180 		Json* n = s in input;
181 		enforce(n !is null, "failed to traverse the input at " ~ s);
182 		input = *n;
183 	}
184 	return input;
185 }
186 
187 unittest {
188 	string t =
189 `{
190 	"name" : {
191 		"foo" : 13
192 	}
193 }`;
194 	Json j = parseJsonString(t);
195 	Json f = j.getWithPath("name");
196 	assert("foo" in f);
197 
198 	f = j.getWithPath("name.foo");
199 	enforce(f.to!int() == 13);
200 
201 	assertThrown(j.getWithPath("doesnotexist"));
202 	assertThrown(j.getWithPath("name.alsoNotThere"));
203 }
204 
205 enum JoinJsonPrecedence {
206 	none,
207 	a,
208 	b
209 }
210 
211 /** Merge two Json objects.
212 Values in a take precedence over values in b.
213 */
214 Json joinJson(JoinJsonPrecedence jjp = JoinJsonPrecedence.none)(Json a, Json b)
215 {
216 	// we can not merge null or undefined values
217 	if(a.type == Json.Type.null_ || a.type == Json.Type.undefined) {
218 		return b;
219 	}
220 	if(b.type == Json.Type.null_ || b.type == Json.Type.undefined) {
221 		return a;
222 	}
223 
224 	// we need objects to merge
225 	if(a.type == Json.Type.object && b.type == Json.Type.object) {
226 		Json ret = a.clone();
227 		foreach(key, value; b.byKeyValue()) {
228 			Json* ap = key in ret;
229 			if(ap is null) {
230 				ret[key] = value;
231 			} else if(ap.type == Json.Type.object
232 					&& value.type == Json.Type.object)
233 			{
234 				ret[key] = joinJson(*ap, value);
235 			} else {
236 				static if(jjp == JoinJsonPrecedence.none) {
237 					throw new Exception(format(
238 							"Can not join '%s' and '%s' on key '%s'",
239 							ap.type, value.type, key));
240 				} else static if(jjp == JoinJsonPrecedence.a) {
241 				} else {
242 					ret[key] = value;
243 				}
244 			}
245 		}
246 		return ret;
247 	}
248 	return a;
249 }
250 
251 unittest {
252 	Json a = parseJsonString(`{"overSize":200}`);
253 	Json b = parseJsonString(`{}`);
254 	const c = joinJson(b, a);
255 	assert(c == a);
256 
257 	b = parseJsonString(`{"underSize":-100}`);
258 	const d = joinJson(b, a);
259 	Json r = parseJsonString(`{"overSize":200, "underSize":-100}`);
260 	assert(d == r);
261 }
262 
263 unittest {
264 	Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
265 			parseJsonString(`{"underSize": {"b": 100}}`)
266 		);
267 
268 	Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`);
269 	assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString()));
270 }
271 
272 unittest {
273 	assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
274 			parseJsonString(`{"underSize": {"a": 100}}`)
275 		));
276 }
277 
278 unittest {
279 	assertThrown(joinJson(parseJsonString(`{"underSize": -100}`),
280 			parseJsonString(`{"underSize": {"a": 100}}`)
281 		));
282 }
283 
284 template toType(T) {
285 	import std.bigint : BigInt;
286 	import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint,
287 		   isSomeString;
288 	static if(is(T == bool)) {
289 		enum toType = Json.Type.bool_;
290 	} else static if(isIntegral!(T)) {
291 		enum toType = Json.Type.int_;
292 	} else static if(isFloatingPoint!(T)) {
293 		enum toType = Json.Type.float_;
294 	} else static if(isSomeString!(T)) {
295 		enum toType = Json.Type..string;
296 	} else static if(isArray!(T)) {
297 		enum toType = Json.Type.array;
298 	} else static if(isAggregateType!(T)) {
299 		enum toType = Json.Type.object;
300 	} else static if(is(T == BigInt)) {
301 		enum toType = Json.Type.bigint;
302 	} else {
303 		enum toType = Json.Type.undefined;
304 	}
305 }
306 
307 bool hasPathTo(T)(Json data, string path, ref T ret) {
308 	enum TT = toType!T;
309 	auto sp = path.splitter(".");
310 	string f;
311 	while(!sp.empty) {
312 		f = sp.front;
313 		sp.popFront();
314 		if(data.type != Json.Type.object || f !in data) {
315 			return false;
316 		} else {
317 			data = data[f];
318 		}
319 	}
320 	static if(is(T == Json)) {
321 		ret = data;
322 		return true;
323 	} else {
324 		if(data.type == TT) {
325 			ret = data.to!T();
326 			return true;
327 		}
328 		return false;
329 	}
330 }
331 
332 unittest {
333 	Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`);
334 	Json ret;
335 	assert(hasPathTo!Json(d, "foo", ret));
336 }
337 
338 /**
339 params:
340 	path = A "." seperated path
341 */
342 T getWithDefault(T)(Json data, string[] paths...) {
343 	enum TT = toType!T;
344 	T ret = T.init;
345 	foreach(string path; paths) {
346 		if(hasPathTo!T(data, path, ret)) {
347 			return ret;
348 		}
349 	}
350 	return ret;
351 }
352 
353 unittest {
354 	Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8,
355 			"__typename":"Starship","series":["DeepSpaceNine",
356 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
357 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`);
358 	const r = d.getWithDefault!string("data.__typename");
359 	assert(r == "Starship", r);
360 }
361 
362 unittest {
363 	Json d = parseJsonString(`{"commanderId":8,
364 			"__typename":"Starship","series":["DeepSpaceNine",
365 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
366 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
367 	const r = d.getWithDefault!string("data.__typename", "__typename");
368 	assert(r == "Starship", r);
369 }
370 
371 unittest {
372 	Json d = parseJsonString(`{"commanderId":8,
373 			"__typename":"Starship","series":["DeepSpaceNine",
374 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
375 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
376 	const r = d.getWithDefault!string("__typename");
377 	assert(r == "Starship", r);
378 }
379 
380 // TODO should return ref
381 auto accessNN(string[] tokens,T)(T tmp0) {
382 	import std.array : back;
383 	import std.format : format;
384 	if(tmp0 !is null) {
385 		static foreach(idx, token; tokens) {
386 			mixin(format!
387 				`if(tmp%d is null) return null;
388 				auto tmp%d = tmp%d.%s;`(idx, idx+1, idx, token)
389 			);
390 		}
391 		return mixin(format("tmp%d", tokens.length));
392 	}
393 	return null;
394 }
395 
396 unittest {
397 	class A {
398 		int a;
399 	}
400 
401 	class B {
402 		A a;
403 	}
404 
405 	class C {
406 		B b;
407 	}
408 
409 	auto c1 = new C;
410 	assert(c1.accessNN!(["b", "a"]) is null);
411 
412 	c1.b = new B;
413 	assert(c1.accessNN!(["b"]) !is null);
414 
415 	assert(c1.accessNN!(["b", "a"]) is null);
416 	// TODO not sure why this is not a lvalue
417 	//c1.accessNN!(["b", "a"]) = new A;
418 	c1.b.a = new A;
419 	assert(c1.accessNN!(["b", "a"]) !is null);
420 }
421 
422 T extract(T)(Json data, string name) {
423 	enforce(data.type == Json.Type.object, format!
424 			"Trying to get a '%s' by name '%s' but passed Json is not an object"
425 			(T.stringof, name)
426 		);
427 
428 	Json* item = name in data;
429 
430 	enforce(item !is null, format!(
431 			"Trying to get a '%s' by name '%s' which is not present in passed "
432 			~ "object '%s'"
433 			)(T.stringof, name, data)
434 		);
435 
436 	return (*item).to!T();
437 }
438 
439 unittest {
440 	import std.exception : assertThrown;
441 	Json j = parseJsonString(`{ "foo": 1337 }`);
442 	auto foo = j.extract!int("foo");
443 
444 	assertThrown(Json.emptyObject().extract!float("Hello"));
445 	assertThrown(j.extract!string("Hello"));
446 }
447 
448 const(Document) lexAndParse(string s) {
449 	import graphql.lexer;
450 	import graphql.parser;
451 	auto l = Lexer(s, QueryParser.no);
452 	auto p = Parser(l);
453 	const(Document) doc = p.parseDocument();
454 	return doc;
455 }
456 
457 struct StringTypeStrip {
458 	string input;
459 	string str;
460 	bool outerNotNull;
461 	bool arr;
462 	bool innerNotNull;
463 
464 	string toString() const {
465 		import std.format : format;
466 		return format("StringTypeStrip(input:'%s', str:'%s', "
467 			   ~ "arr:'%s', outerNotNull:'%s', innerNotNull:'%s')",
468 			   this.input, this.str, this.arr, this.outerNotNull,
469 			   this.innerNotNull);
470 	}
471 }
472 
473 StringTypeStrip stringTypeStrip(string str) {
474 	Nullable!StringTypeStrip gqld = gqldStringTypeStrip(str);
475 	return gqld.get();
476 	//return gqld.isNull()
477 	//	? dlangStringTypeStrip(str)
478 	//	: gqld.get();
479 }
480 
481 private Nullable!StringTypeStrip gqldStringTypeStrip(string str) {
482 	StringTypeStrip ret;
483 	ret.input = str;
484 	string old = str;
485 	bool firstBang;
486 	if(str.endsWith('!')) {
487 		firstBang = true;
488 		str = str[0 .. $ - 1];
489 	}
490 
491 	bool arr;
492 	if(str.startsWith('[') && str.endsWith(']')) {
493 		arr = true;
494 		str = str[1 .. $ - 1];
495 	}
496 
497 	bool secondBang;
498 	if(str.endsWith('!')) {
499 		secondBang = true;
500 		str = str[0 .. $ - 1];
501 	}
502 
503 	if(arr) {
504 		ret.innerNotNull = secondBang;
505 		ret.outerNotNull = firstBang;
506 	} else {
507 		ret.innerNotNull = firstBang;
508 	}
509 
510 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
511 		? "Int"
512 		: str;
513 
514 	str = canFind(["string", "int", "float", "bool"], str)
515 		? capitalize(str)
516 		: str;
517 
518 	str = str == "__type" ? "__Type" : str;
519 	str = str == "__schema" ? "__Schema" : str;
520 	str = str == "__inputvalue" ? "__InputValue" : str;
521 	str = str == "__directive" ? "__Directive" : str;
522 	str = str == "__field" ? "__Field" : str;
523 
524 	ret.arr = arr;
525 
526 	ret.str = str;
527 	//writefln("%s %s", __LINE__, ret);
528 
529 	//return old == str ? Nullable!(StringTypeStrip).init : nullable(ret);
530 	return nullable(ret);
531 }
532 
533 unittest {
534 	auto a = gqldStringTypeStrip("String");
535 	assert(!a.isNull());
536 
537 	a = gqldStringTypeStrip("String!");
538 	assert(!a.isNull());
539 	assert(a.get().str == "String");
540 	assert(a.get().innerNotNull, format("%s", a.get()));
541 
542 	a = gqldStringTypeStrip("[String!]");
543 	assert(!a.isNull());
544 	assert(a.get().str == "String");
545 	assert(a.get().arr, format("%s", a.get()));
546 	assert(a.get().innerNotNull, format("%s", a.get()));
547 
548 	a = gqldStringTypeStrip("[String]!");
549 	assert(!a.isNull());
550 	assert(a.get().str == "String");
551 	assert(a.get().arr, format("%s", a.get()));
552 	assert(!a.get().innerNotNull, format("%s", a.get()));
553 	assert(a.get().outerNotNull, format("%s", a.get()));
554 
555 	a = gqldStringTypeStrip("[String!]!");
556 	assert(!a.isNull());
557 	assert(a.get().str == "String");
558 	assert(a.get().arr, format("%s", a.get()));
559 	assert(a.get().innerNotNull, format("%s", a.get()));
560 	assert(a.get().outerNotNull, format("%s", a.get()));
561 }
562 
563 private StringTypeStrip dlangStringTypeStrip(string str) {
564 	StringTypeStrip ret;
565 	ret.outerNotNull = true;
566 	ret.innerNotNull = true;
567 	ret.input = str;
568 
569 	immutable ns = "NullableStore!";
570 	immutable ns1 = "NullableStore!(";
571 	immutable leaf = "GQLDCustomLeaf!";
572 	immutable leaf1 = "GQLDCustomLeaf!(";
573 	immutable nll = "Nullable!";
574 	immutable nll1 = "Nullable!(";
575 
576 	// NullableStore!( .... )
577 	if(str.startsWith(ns1) && str.endsWith(")")) {
578 		str = str[ns1.length .. $ - 1];
579 	}
580 
581 	// NullableStore!....
582 	if(str.startsWith(ns)) {
583 		str = str[ns.length .. $];
584 	}
585 
586 	// GQLDCustomLeaf!( .... )
587 	if(str.startsWith(leaf1) && str.endsWith(")")) {
588 		str = str[leaf1.length .. $ - 1];
589 	}
590 
591 	bool firstNull;
592 
593 	// Nullable!( .... )
594 	if(str.startsWith(nll1) && str.endsWith(")")) {
595 		firstNull = true;
596 		str = str[nll1.length .. $ - 1];
597 	}
598 
599 	// NullableStore!( .... )
600 	if(str.startsWith(ns1) && str.endsWith(")")) {
601 		str = str[ns1.length .. $ - 1];
602 	}
603 
604 	// NullableStore!....
605 	if(str.startsWith(ns)) {
606 		str = str[ns.length .. $];
607 	}
608 
609 	if(str.endsWith("!")) {
610 		str = str[0 .. $ - 1];
611 	}
612 
613 	// xxxxxxx[]
614 	if(str.endsWith("[]")) {
615 		ret.arr = true;
616 		str = str[0 .. $ - 2];
617 	}
618 
619 	bool secondNull;
620 
621 	// Nullable!( .... )
622 	if(str.startsWith(nll1) && str.endsWith(")")) {
623 		secondNull = true;
624 		str = str[nll1.length .. $ - 1];
625 	}
626 
627 	if(str.endsWith("!")) {
628 		str = str[0 .. $ - 1];
629 	}
630 
631 	// Nullable! ....
632 	if(str.startsWith(nll)) {
633 		secondNull = true;
634 		str = str[nll.length .. $];
635 	}
636 
637 	// NullableStore!( .... )
638 	if(str.startsWith(ns1) && str.endsWith(")")) {
639 		str = str[ns1.length .. $ - 1];
640 	}
641 
642 	// NullableStore!....
643 	if(str.startsWith(ns)) {
644 		str = str[ns.length .. $];
645 	}
646 
647 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
648 		? "Int"
649 		: str;
650 
651 	str = canFind(["string", "int", "float", "bool"], str)
652 		? capitalize(str)
653 		: str;
654 
655 	str = str == "__type" ? "__Type" : str;
656 	str = str == "__schema" ? "__Schema" : str;
657 	str = str == "__inputvalue" ? "__InputValue" : str;
658 	str = str == "__directive" ? "__Directive" : str;
659 	str = str == "__field" ? "__Field" : str;
660 
661 	//writefln("firstNull %s, secondNull %s, arr %s", firstNull, secondNull,
662 	//		ret.arr);
663 
664 	if(ret.arr) {
665 		ret.innerNotNull = !secondNull;
666 		ret.outerNotNull = !firstNull;
667 	} else {
668 		ret.innerNotNull = !secondNull;
669 	}
670 
671 	ret.str = str;
672 	return ret;
673 }
674 
675 unittest {
676 	bool oNN;
677 	bool arr;
678 	bool iNN;
679 	string t = "Nullable!string";
680 	StringTypeStrip r = t.dlangStringTypeStrip();
681 	assert(r.str == "String", to!string(r));
682 	assert(!r.arr, to!string(r));
683 	assert(!r.innerNotNull, to!string(r));
684 	assert(r.outerNotNull, to!string(r));
685 
686 	t = "Nullable!(string[])";
687 	r = t.dlangStringTypeStrip();
688 	assert(r.str == "String", to!string(r));
689 	assert(r.arr, to!string(r));
690 	assert(r.innerNotNull, to!string(r));
691 	assert(!r.outerNotNull, to!string(r));
692 }
693 
694 unittest {
695 	string t = "Nullable!__type";
696 	StringTypeStrip r = t.dlangStringTypeStrip();
697 	assert(r.str == "__Type", to!string(r));
698 	assert(!r.innerNotNull, to!string(r));
699 	assert(r.outerNotNull, to!string(r));
700 	assert(!r.arr, to!string(r));
701 
702 	t = "Nullable!(__type[])";
703 	r = t.dlangStringTypeStrip();
704 	assert(r.str == "__Type", to!string(r));
705 	assert(r.innerNotNull, to!string(r));
706 	assert(!r.outerNotNull, to!string(r));
707 	assert(r.arr, to!string(r));
708 }
709 
710 template isClass(T) {
711 	enum isClass = is(T == class);
712 }
713 
714 unittest {
715 	static assert(!isClass!int);
716 	static assert( isClass!Object);
717 }
718 
719 template isNotInTypeSet(T, R...) {
720 	import std.meta : staticIndexOf;
721 	enum isNotInTypeSet = staticIndexOf!(T, R) == -1;
722 }
723 
724 string getTypename(Schema,T)(auto ref T input) @trusted {
725 	//pragma(msg, T);
726 	//writefln("To %s", T.stringof);
727 	static if(!isClass!(T)) {
728 		return T.stringof;
729 	} else {
730 		// fetch the typeinfo of the item, and compare it down until we get to a
731 		// class we have. If none found, return the name of the type itself.
732 		import graphql.reflection;
733 		auto tinfo = typeid(input);
734 		auto reflect = SchemaReflection!Schema.instance;
735 		while(tinfo !is null) {
736 			if(auto cname = tinfo in reflect.classes) {
737 				return *cname;
738 			}
739 			tinfo = tinfo.base;
740 		}
741 		return T.stringof;
742 	}
743 }
744 
745 Json toGraphqlJson(Schema,T)(auto ref T input) {
746 	import std.array : empty;
747 	import std.conv : to;
748 	import std.typecons : Nullable;
749 	import std.traits : isArray, isAggregateType, isBasicType, isSomeString,
750 		   isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple;
751 
752 	import nullablestore;
753 
754 	import graphql.uda : GQLDCustomLeaf;
755 	static if(isArray!T && !isSomeString!T) {
756 		Json ret = Json.emptyArray();
757 		foreach(ref it; input) {
758 			ret ~= toGraphqlJson!Schema(it);
759 		}
760 		return ret;
761 	} else static if(is(T : GQLDCustomLeaf!Type, Type...)) {
762 		return Json(Type[1](input));
763 	} else static if(is(T : Nullable!Type, Type)) {
764 		return input.isNull() ? Json(null) : toGraphqlJson!Schema(input.get());
765 	} else static if(is(T == enum)) {
766 		return Json(to!string(input));
767 	} else static if(isBasicType!T || isScalarType!T || isSomeString!T) {
768 		return serializeToJson(input);
769 	} else static if(isAggregateType!T) {
770 		Json ret = Json.emptyObject();
771 
772 		// the important bit is the setting of the __typename field
773 		ret["__typename"] = getTypename!(Schema)(input);
774 		//writefln("Got %s", ret["__typename"].to!string());
775 
776 		alias names = FieldNameTuple!(T);
777 		alias types = FieldTypeTuple!(T);
778 		static foreach(idx; 0 .. names.length) {{
779 			static if(!names[idx].empty) {
780 				static if(is(types[idx] : NullableStore!Type, Type)) {
781 				} else static if(is(types[idx] == enum)) {
782 					ret[names[idx]] =
783 						to!string(__traits(getMember, input, names[idx]));
784 				} else {
785 					ret[names[idx]] = toGraphqlJson!Schema(
786 							__traits(getMember, input, names[idx])
787 						);
788 				}
789 			}
790 		}}
791 		return ret;
792 	} else {
793 		static assert(false, T.stringof ~ " not supported");
794 	}
795 }
796 
797 string dtToString(DateTime dt) {
798 	return dt.toISOExtString();
799 }
800 
801 string dToString(Date dt) {
802 	return dt.toISOExtString();
803 }
804 
805 unittest {
806 	import std.typecons : nullable, Nullable;
807 	import graphql.uda;
808 	import nullablestore;
809 
810 	struct Foo {
811 		int a;
812 		Nullable!int b;
813 		NullableStore!float c;
814 		GQLDCustomLeaf!(DateTime, dtToString) dt2;
815 		Nullable!(GQLDCustomLeaf!(DateTime, dtToString)) dt;
816 	}
817 
818 	DateTime dt = DateTime(1337, 7, 1, 1, 1, 1);
819 	DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3);
820 
821 	Foo foo;
822 	foo.dt2 = GQLDCustomLeaf!(DateTime, dtToString)(dt2);
823 	foo.dt = nullable(GQLDCustomLeaf!(DateTime, dtToString)(dt));
824 	Json j = toGraphqlJson!int(foo);
825 	assert(j["a"].to!int() == 0);
826 	assert(j["b"].type == Json.Type.null_);
827 	assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type,
828 				j.toPrettyString()
829 			)
830 		);
831 	string exp = j["dt"].to!string();
832 	assert(exp == "1337-07-01T01:01:01", exp);
833 	string exp2 = j["dt2"].to!string();
834 	assert(exp2 == "2337-07-01T01:01:03", exp2);
835 }
836 
837 struct PathElement {
838 	string str;
839 	size_t idx;
840 
841 	static PathElement opCall(string s) {
842 		PathElement ret;
843 		ret.str = s;
844 		return ret;
845 	}
846 
847 	static PathElement opCall(size_t s) {
848 		PathElement ret;
849 		ret.idx = s;
850 		return ret;
851 	}
852 
853 	Json toJson() {
854 		return this.str.empty ? Json(this.idx) : Json(this.str);
855 	}
856 }
857