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.format : format;
10 import std.stdio;
11 import std.string : capitalize, indexOf, strip;
12 import std.typecons : nullable, Nullable;
13 
14 import vibe.data.json;
15 
16 import graphql.ast;
17 import graphql.uda;
18 import graphql.constants;
19 import graphql.exception;
20 
21 @safe:
22 
23 /** dmd and ldc have problems with generation all functions
24 This functions call functions that were undefined.
25 */
26 private void undefinedFunctions() @trusted {
27 	static import core.internal.hash;
28 	static import graphql.schema.introspectiontypes;
29 
30 	const(graphql.schema.introspectiontypes.__Type)[] tmp;
31 	core.internal.hash.hashOf!(const(graphql.schema.introspectiontypes.__Type)[])
32 		(tmp, 0);
33 }
34 
35 enum d = "data";
36 enum e = Constants.errors;
37 
38 string firstCharUpperCase(string input) {
39 	import std.conv : to;
40 	import std.uni : isUpper, toUpper;
41 	import std.array : front, popFront;
42 	if(isUpper(input.front)) {
43 		return input;
44 	}
45 
46 	const f = input.front;
47 	input.popFront();
48 
49 	return to!string(toUpper(f)) ~ input;
50 }
51 
52 Json returnTemplate() {
53 	Json ret = Json.emptyObject();
54 	ret["data"] = Json.emptyObject();
55 	ret[Constants.errors] = Json.emptyArray();
56 	return ret;
57 }
58 
59 void insertError(T)(ref Json result, T t) {
60 	insertError(result, t, []);
61 }
62 
63 void insertError(T)(ref Json result, T t, PathElement[] path) {
64 	Json tmp = Json.emptyObject();
65 	tmp["message"] = serializeToJson(t);
66 	if(!path.empty) {
67 		tmp["path"] = Json.emptyArray();
68 		foreach(it; path) {
69 			tmp["path"] ~= it.toJson();
70 		}
71 	}
72 	if(e !in result) {
73 		result[e] = Json.emptyArray();
74 	}
75 	enforce(result[e].type == Json.Type.array);
76 	result[e] ~= tmp;
77 }
78 
79 void insertPayload(ref Json result, string field, Json data) {
80 	if(d in data) {
81 		if(d !in result) {
82 			result[d] = Json.emptyObject();
83 		}
84 		enforce(result[d].type == Json.Type.object);
85 		Json* df = field in result[d];
86 		if(df) {
87 			result[d][field] = joinJson(*df, data[d]);
88 		} else {
89 			result[d][field] = data[d];
90 		}
91 	}
92 	if(e in data) {
93 		if(e !in result) {
94 			result[e] = Json.emptyArray();
95 		}
96 		enforce(result[e].type == Json.Type.array);
97 		if(!canFind(result[e].byValue(), data[e])) {
98 			result[e] ~= data[e];
99 		}
100 	}
101 }
102 
103 unittest {
104 	Json old = returnTemplate();
105 	old["data"]["foo"] = Json.emptyObject();
106 	old["data"]["foo"]["a"] = 1337;
107 
108 	Json n = returnTemplate();
109 	n["data"] = Json.emptyObject();
110 	n["data"]["b"] = 1338;
111 
112 	old.insertPayload("foo", n);
113 	assert(old["data"]["foo"].length == 2, format("%s %s",
114 			old["data"]["foo"].length, old.toPrettyString())
115 		);
116 	assert("a" in old["data"]["foo"]);
117 	assert("b" in old["data"]["foo"]);
118 }
119 
120 bool isScalar(ref const(Json) data) {
121 	return data.type == Json.Type.bigInt
122 			|| data.type == Json.Type.bool_
123 			|| data.type == Json.Type.float_
124 			|| data.type == Json.Type.int_
125 			|| data.type == Json.Type..string;
126 }
127 
128 bool dataIsEmpty(ref const(Json) data) {
129 	if(data.type == Json.Type.object) {
130 		foreach(key, value; data.byKeyValue()) {
131 			if(key != Constants.errors && !value.dataIsEmpty()) {
132 			//if(key != Constants.errors) { // Issue #22 place to look at
133 				return false;
134 			}
135 		}
136 		return true;
137 	} else if(data.type == Json.Type.null_
138 			|| data.type == Json.Type.undefined
139 		)
140 	{
141 		return true;
142 	} else if(data.type == Json.Type.array) {
143 		return data.length == 0;
144 	} else if(data.type == Json.Type.bigInt
145 			|| data.type == Json.Type.bool_
146 			|| data.type == Json.Type.float_
147 			|| data.type == Json.Type.int_
148 			|| data.type == Json.Type..string
149 		)
150 	{
151 		return false;
152 	}
153 
154 	return true;
155 }
156 
157 unittest {
158 	string t = `{ "errors" : {} }`;
159 	Json j = parseJsonString(t);
160 	assert(j.dataIsEmpty());
161 }
162 
163 unittest {
164 	string t = `{ "kind": {}, "fields": null, "name": {} }`;
165 	Json j = parseJsonString(t);
166 	//assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22
167 	assert(j.dataIsEmpty());
168 }
169 
170 unittest {
171 	string t =
172 `{
173 	"name" : {
174 		"foo" : null
175 	}
176 }`;
177 	Json j = parseJsonString(t);
178 	//assert(!j.dataIsEmpty()); // Enable if you don't want to trim. Issue #22
179 	assert(j.dataIsEmpty());
180 }
181 
182 bool dataIsNull(ref const(Json) data) {
183 	import std.format : format;
184 	enforce(data.type == Json.Type.object, format("%s", data));
185 	if(const(Json)* d = "data" in data) {
186 		return d.type == Json.Type.null_;
187 	}
188 	return false;
189 }
190 
191 Json getWithPath(Json input, string path) {
192 	auto sp = path.splitter(".");
193 	foreach(s; sp) {
194 		Json* n = s in input;
195 		enforce(n !is null, "failed to traverse the input at " ~ s);
196 		input = *n;
197 	}
198 	return input;
199 }
200 
201 unittest {
202 	string t =
203 `{
204 	"name" : {
205 		"foo" : 13
206 	}
207 }`;
208 	Json j = parseJsonString(t);
209 	Json f = j.getWithPath("name");
210 	assert("foo" in f);
211 
212 	f = j.getWithPath("name.foo");
213 	enforce(f.to!int() == 13);
214 
215 	assertThrown(j.getWithPath("doesnotexist"));
216 	assertThrown(j.getWithPath("name.alsoNotThere"));
217 }
218 
219 enum JoinJsonPrecedence {
220 	none,
221 	a,
222 	b
223 }
224 
225 /** Merge two Json objects.
226 Values in a take precedence over values in b.
227 */
228 Json joinJson(JoinJsonPrecedence jjp = JoinJsonPrecedence.none)(Json a, Json b)
229 {
230 	// we can not merge null or undefined values
231 	if(a.type == Json.Type.null_ || a.type == Json.Type.undefined) {
232 		return b;
233 	}
234 	if(b.type == Json.Type.null_ || b.type == Json.Type.undefined) {
235 		return a;
236 	}
237 
238 	// we need objects to merge
239 	if(a.type == Json.Type.object && b.type == Json.Type.object) {
240 		Json ret = a.clone();
241 		foreach(key, value; b.byKeyValue()) {
242 			Json* ap = key in ret;
243 			if(ap is null) {
244 				ret[key] = value;
245 			} else if(ap.type == Json.Type.object
246 					&& value.type == Json.Type.object)
247 			{
248 				ret[key] = joinJson(*ap, value);
249 			} else {
250 				static if(jjp == JoinJsonPrecedence.none) {
251 					throw new Exception(format(
252 							"Can not join '%s' and '%s' on key '%s'",
253 							ap.type, value.type, key));
254 				} else static if(jjp == JoinJsonPrecedence.a) {
255 				} else {
256 					ret[key] = value;
257 				}
258 			}
259 		}
260 		return ret;
261 	}
262 	return a;
263 }
264 
265 unittest {
266 	Json a = parseJsonString(`{"overSize":200}`);
267 	Json b = parseJsonString(`{}`);
268 	const c = joinJson(b, a);
269 	assert(c == a);
270 
271 	b = parseJsonString(`{"underSize":-100}`);
272 	const d = joinJson(b, a);
273 	immutable Json r = parseJsonString(`{"overSize":200, "underSize":-100}`);
274 	assert(d == r);
275 }
276 
277 unittest {
278 	Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
279 			parseJsonString(`{"underSize": {"b": 100}}`)
280 		);
281 
282 	Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`);
283 	assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString()));
284 }
285 
286 unittest {
287 	assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
288 			parseJsonString(`{"underSize": {"a": 100}}`)
289 		));
290 }
291 
292 unittest {
293 	assertThrown(joinJson(parseJsonString(`{"underSize": -100}`),
294 			parseJsonString(`{"underSize": {"a": 100}}`)
295 		));
296 }
297 
298 template toType(T) {
299 	import std.bigint : BigInt;
300 	import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint,
301 		   isSomeString;
302 	static if(is(T == bool)) {
303 		enum toType = Json.Type.bool_;
304 	} else static if(isIntegral!(T)) {
305 		enum toType = Json.Type.int_;
306 	} else static if(isFloatingPoint!(T)) {
307 		enum toType = Json.Type.float_;
308 	} else static if(isSomeString!(T)) {
309 		enum toType = Json.Type..string;
310 	} else static if(isArray!(T)) {
311 		enum toType = Json.Type.array;
312 	} else static if(isAggregateType!(T)) {
313 		enum toType = Json.Type.object;
314 	} else static if(is(T == BigInt)) {
315 		enum toType = Json.Type.bigint;
316 	} else {
317 		enum toType = Json.Type.undefined;
318 	}
319 }
320 
321 bool hasPathTo(T)(Json data, string path, ref T ret) {
322 	enum TT = toType!T;
323 	auto sp = path.splitter(".");
324 	string f;
325 	while(!sp.empty) {
326 		f = sp.front;
327 		sp.popFront();
328 		if(data.type != Json.Type.object || f !in data) {
329 			return false;
330 		} else {
331 			data = data[f];
332 		}
333 	}
334 	static if(is(T == Json)) {
335 		ret = data;
336 		return true;
337 	} else {
338 		if(data.type == TT) {
339 			ret = data.to!T();
340 			return true;
341 		}
342 		return false;
343 	}
344 }
345 
346 unittest {
347 	Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`);
348 	Json ret;
349 	assert(hasPathTo!Json(d, "foo", ret));
350 	assert("path" in ret);
351 	assert(ret["path"].type == Json.Type..string);
352 	assert(ret["path"].get!string() == "foo");
353 }
354 
355 /**
356 params:
357 	path = A "." seperated path
358 */
359 T getWithDefault(T)(Json data, string[] paths...) {
360 	enum TT = toType!T;
361 	T ret = T.init;
362 	foreach(string path; paths) {
363 		if(hasPathTo!T(data, path, ret)) {
364 			return ret;
365 		}
366 	}
367 	return ret;
368 }
369 
370 unittest {
371 	Json d = parseJsonString(`{"errors":[],"data":{"commanderId":8,
372 			"__typename":"Starship","series":["DeepSpaceNine",
373 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
374 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}}`);
375 	const r = d.getWithDefault!string("data.__typename");
376 	assert(r == "Starship", r);
377 }
378 
379 unittest {
380 	Json d = parseJsonString(`{"commanderId":8,
381 			"__typename":"Starship","series":["DeepSpaceNine",
382 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
383 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
384 	const r = d.getWithDefault!string("data.__typename", "__typename");
385 	assert(r == "Starship", r);
386 }
387 
388 unittest {
389 	Json d = parseJsonString(`{"commanderId":8,
390 			"__typename":"Starship","series":["DeepSpaceNine",
391 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
392 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
393 	const r = d.getWithDefault!string("__typename");
394 	assert(r == "Starship", r);
395 }
396 
397 // TODO should return ref
398 auto accessNN(string[] tokens,T)(T tmp0) {
399 	import std.array : back;
400 	import std.format : format;
401 	if(tmp0 !is null) {
402 		static foreach(idx, token; tokens) {
403 			mixin(format(
404 				`if(tmp%d is null) return null;
405 				auto tmp%d = tmp%d.%s;`, idx, idx+1, idx, token)
406 			);
407 		}
408 		return mixin(format("tmp%d", tokens.length));
409 	}
410 	return null;
411 }
412 
413 unittest {
414 	class A {
415 		int a;
416 	}
417 
418 	class B {
419 		A a;
420 	}
421 
422 	class C {
423 		B b;
424 	}
425 
426 	auto c1 = new C;
427 	assert(c1.accessNN!(["b", "a"]) is null);
428 
429 	c1.b = new B;
430 	assert(c1.accessNN!(["b"]) !is null);
431 
432 	assert(c1.accessNN!(["b", "a"]) is null);
433 	// TODO not sure why this is not a lvalue
434 	//c1.accessNN!(["b", "a"]) = new A;
435 	c1.b.a = new A;
436 	assert(c1.accessNN!(["b", "a"]) !is null);
437 }
438 
439 T jsonTo(T)(Json item) {
440 	static import std.conv;
441 	static if(is(T == enum)) {
442 		enforce!GQLDExecutionException(item.type == Json.Type..string,
443 			format("Enum '%s' must be passed as string not '%s'",
444 				T.stringof, item.type));
445 
446 		string s = item.to!string();
447 		try {
448 			return std.conv.to!T(s);
449 		} catch(Exception c) {
450 			throw new GQLDExecutionException(c.msg);
451 		}
452 	} else static if(is(T == GQLDCustomLeaf!Fs, Fs...)) {
453 		enforce!GQLDExecutionException(item.type == Json.Type..string,
454 			format("%1$s '%1$s' must be passed as string not '%2$s'",
455 				T.stringof, item.type));
456 
457 		string s = item.to!string();
458 		try {
459 			return T(Fs[2](s));
460 		} catch(Exception c) {
461 			throw new GQLDExecutionException(c.msg);
462 		}
463 	} else {
464 		try {
465 			return item.to!T();
466 		} catch(Exception c) {
467 			throw new GQLDExecutionException(c.msg);
468 		}
469 	}
470 }
471 
472 T extract(T)(Json data, string name) {
473 	enforce!GQLDExecutionException(data.type == Json.Type.object, format(
474 			"Trying to get a '%s' by name '%s' but passed Json is not an object"
475 			, T.stringof, name)
476 		);
477 
478 	Json* item = name in data;
479 
480 	enforce!GQLDExecutionException(item !is null, format(
481 			"Trying to get a '%s' by name '%s' which is not present in passed "
482 			~ "object '%s'"
483 			, T.stringof, name, data)
484 		);
485 
486 	return jsonTo!(T)(*item);
487 }
488 
489 unittest {
490 	import std.exception : assertThrown;
491 	Json j = parseJsonString(`null`);
492 	assertThrown(j.extract!string("Hello"));
493 }
494 
495 unittest {
496 	enum E {
497 		no,
498 		yes
499 	}
500 	import std.exception : assertThrown;
501 	Json j = parseJsonString(`{ "foo": 1337 }`);
502 
503 	assertThrown(j.extract!E("foo"));
504 
505 	j = parseJsonString(`{ "foo": "str" }`);
506 	assertThrown(j.extract!E("foo"));
507 
508 	j = parseJsonString(`{ "foo": "yes" }`);
509 	assert(j.extract!E("foo") == E.yes);
510 }
511 
512 unittest {
513 	import std.exception : assertThrown;
514 	Json j = parseJsonString(`{ "foo": 1337 }`);
515 	immutable auto foo = j.extract!int("foo");
516 
517 	assertThrown(Json.emptyObject().extract!float("Hello"));
518 	assertThrown(j.extract!string("Hello"));
519 }
520 
521 unittest {
522 	import std.exception : assertThrown;
523 	enum FooEn {
524 		a,
525 		b
526 	}
527 	Json j = parseJsonString(`{ "foo": "a" }`);
528 	immutable auto foo = j.extract!FooEn("foo");
529 	assert(foo == FooEn.a);
530 
531 	assertThrown(Json.emptyObject().extract!float("Hello"));
532 	assertThrown(j.extract!string("Hello"));
533 	assert(j["foo"].jsonTo!FooEn() == FooEn.a);
534 
535 	Json k = parseJsonString(`{ "foo": "b" }`);
536 	assert(k["foo"].jsonTo!FooEn() == FooEn.b);
537 }
538 
539 const(Document) lexAndParse(string s) {
540 	import graphql.lexer;
541 	import graphql.parser;
542 	auto l = Lexer(s, QueryParser.no);
543 	auto p = Parser(l);
544 	const(Document) doc = p.parseDocument();
545 	return doc;
546 }
547 
548 struct StringTypeStrip {
549 	string input;
550 	string str;
551 	bool outerNotNull;
552 	bool arr;
553 	bool innerNotNull;
554 
555 	string toString() const {
556 		import std.format : format;
557 		return format("StringTypeStrip(input:'%s', str:'%s', "
558 			   ~ "arr:'%s', outerNotNull:'%s', innerNotNull:'%s')",
559 			   this.input, this.str, this.arr, this.outerNotNull,
560 			   this.innerNotNull);
561 	}
562 }
563 
564 StringTypeStrip stringTypeStrip(string str) {
565 	Nullable!StringTypeStrip gqld = gqldStringTypeStrip(str);
566 	return gqld.get();
567 	//return gqld.isNull()
568 	//	? dlangStringTypeStrip(str)
569 	//	: gqld.get();
570 }
571 
572 private Nullable!StringTypeStrip gqldStringTypeStrip(string str) {
573 	StringTypeStrip ret;
574 	ret.input = str;
575 	immutable string old = str;
576 	bool firstBang;
577 	if(str.endsWith('!')) {
578 		firstBang = true;
579 		str = str[0 .. $ - 1];
580 	}
581 
582 	bool arr;
583 	if(str.startsWith('[') && str.endsWith(']')) {
584 		arr = true;
585 		str = str[1 .. $ - 1];
586 	}
587 
588 	bool secondBang;
589 	if(str.endsWith('!')) {
590 		secondBang = true;
591 		str = str[0 .. $ - 1];
592 	}
593 
594 	if(arr) {
595 		ret.innerNotNull = secondBang;
596 		ret.outerNotNull = firstBang;
597 	} else {
598 		ret.innerNotNull = firstBang;
599 	}
600 
601 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
602 		? "Int"
603 		: str;
604 
605 	str = canFind(["string", "int", "float", "bool"], str)
606 		? capitalize(str)
607 		: str;
608 
609 	str = str == "__type" ? "__Type" : str;
610 	str = str == "__schema" ? "__Schema" : str;
611 	str = str == "__inputvalue" ? "__InputValue" : str;
612 	str = str == "__directive" ? "__Directive" : str;
613 	str = str == "__field" ? "__Field" : str;
614 
615 	ret.arr = arr;
616 
617 	ret.str = str;
618 	//writefln("%s %s", __LINE__, ret);
619 
620 	//return old == str ? Nullable!(StringTypeStrip).init : nullable(ret);
621 	return nullable(ret);
622 }
623 
624 unittest {
625 	auto a = gqldStringTypeStrip("String");
626 	assert(!a.isNull());
627 
628 	a = gqldStringTypeStrip("String!");
629 	assert(!a.isNull());
630 	assert(a.get().str == "String");
631 	assert(a.get().innerNotNull, format("%s", a.get()));
632 
633 	a = gqldStringTypeStrip("[String!]");
634 	assert(!a.isNull());
635 	assert(a.get().str == "String");
636 	assert(a.get().arr, format("%s", a.get()));
637 	assert(a.get().innerNotNull, format("%s", a.get()));
638 
639 	a = gqldStringTypeStrip("[String]!");
640 	assert(!a.isNull());
641 	assert(a.get().str == "String");
642 	assert(a.get().arr, format("%s", a.get()));
643 	assert(!a.get().innerNotNull, format("%s", a.get()));
644 	assert(a.get().outerNotNull, format("%s", a.get()));
645 
646 	a = gqldStringTypeStrip("[String!]!");
647 	assert(!a.isNull());
648 	assert(a.get().str == "String");
649 	assert(a.get().arr, format("%s", a.get()));
650 	assert(a.get().innerNotNull, format("%s", a.get()));
651 	assert(a.get().outerNotNull, format("%s", a.get()));
652 }
653 
654 private StringTypeStrip dlangStringTypeStrip(string str) {
655 	StringTypeStrip ret;
656 	ret.outerNotNull = true;
657 	ret.innerNotNull = true;
658 	ret.input = str;
659 
660 	immutable ns = "NullableStore!";
661 	immutable ns1 = "NullableStore!(";
662 	immutable leaf = "GQLDCustomLeaf!";
663 	immutable leaf1 = "GQLDCustomLeaf!(";
664 	immutable nll = "Nullable!";
665 	immutable nll1 = "Nullable!(";
666 
667 	// NullableStore!( .... )
668 	if(str.startsWith(ns1) && str.endsWith(")")) {
669 		str = str[ns1.length .. $ - 1];
670 	}
671 
672 	// NullableStore!....
673 	if(str.startsWith(ns)) {
674 		str = str[ns.length .. $];
675 	}
676 
677 	// GQLDCustomLeaf!( .... )
678 	if(str.startsWith(leaf1) && str.endsWith(")")) {
679 		str = str[leaf1.length .. $ - 1];
680 	}
681 
682 	bool firstNull;
683 
684 	// Nullable!( .... )
685 	if(str.startsWith(nll1) && str.endsWith(")")) {
686 		firstNull = true;
687 		str = str[nll1.length .. $ - 1];
688 	}
689 
690 	// NullableStore!( .... )
691 	if(str.startsWith(ns1) && str.endsWith(")")) {
692 		str = str[ns1.length .. $ - 1];
693 	}
694 
695 	// NullableStore!....
696 	if(str.startsWith(ns)) {
697 		str = str[ns.length .. $];
698 	}
699 
700 	if(str.endsWith("!")) {
701 		str = str[0 .. $ - 1];
702 	}
703 
704 	// xxxxxxx[]
705 	if(str.endsWith("[]")) {
706 		ret.arr = true;
707 		str = str[0 .. $ - 2];
708 	}
709 
710 	bool secondNull;
711 
712 	// Nullable!( .... )
713 	if(str.startsWith(nll1) && str.endsWith(")")) {
714 		secondNull = true;
715 		str = str[nll1.length .. $ - 1];
716 	}
717 
718 	if(str.endsWith("!")) {
719 		str = str[0 .. $ - 1];
720 	}
721 
722 	// Nullable! ....
723 	if(str.startsWith(nll)) {
724 		secondNull = true;
725 		str = str[nll.length .. $];
726 	}
727 
728 	// NullableStore!( .... )
729 	if(str.startsWith(ns1) && str.endsWith(")")) {
730 		str = str[ns1.length .. $ - 1];
731 	}
732 
733 	// NullableStore!....
734 	if(str.startsWith(ns)) {
735 		str = str[ns.length .. $];
736 	}
737 
738 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
739 		? "Int"
740 		: str;
741 
742 	str = canFind(["string", "int", "float", "bool"], str)
743 		? capitalize(str)
744 		: str;
745 
746 	str = str == "__type" ? "__Type" : str;
747 	str = str == "__schema" ? "__Schema" : str;
748 	str = str == "__inputvalue" ? "__InputValue" : str;
749 	str = str == "__directive" ? "__Directive" : str;
750 	str = str == "__field" ? "__Field" : str;
751 
752 	//writefln("firstNull %s, secondNull %s, arr %s", firstNull, secondNull,
753 	//		ret.arr);
754 
755 	if(ret.arr) {
756 		ret.innerNotNull = !secondNull;
757 		ret.outerNotNull = !firstNull;
758 	} else {
759 		ret.innerNotNull = !secondNull;
760 	}
761 
762 	ret.str = str;
763 	return ret;
764 }
765 
766 unittest {
767 	string t = "Nullable!string";
768 	StringTypeStrip r = t.dlangStringTypeStrip();
769 	assert(r.str == "String", to!string(r));
770 	assert(!r.arr, to!string(r));
771 	assert(!r.innerNotNull, to!string(r));
772 	assert(r.outerNotNull, to!string(r));
773 
774 	t = "Nullable!(string[])";
775 	r = t.dlangStringTypeStrip();
776 	assert(r.str == "String", to!string(r));
777 	assert(r.arr, to!string(r));
778 	assert(r.innerNotNull, to!string(r));
779 	assert(!r.outerNotNull, to!string(r));
780 }
781 
782 unittest {
783 	string t = "Nullable!__type";
784 	StringTypeStrip r = t.dlangStringTypeStrip();
785 	assert(r.str == "__Type", to!string(r));
786 	assert(!r.innerNotNull, to!string(r));
787 	assert(r.outerNotNull, to!string(r));
788 	assert(!r.arr, to!string(r));
789 
790 	t = "Nullable!(__type[])";
791 	r = t.dlangStringTypeStrip();
792 	assert(r.str == "__Type", to!string(r));
793 	assert(r.innerNotNull, to!string(r));
794 	assert(!r.outerNotNull, to!string(r));
795 	assert(r.arr, to!string(r));
796 }
797 
798 template isClass(T) {
799 	enum isClass = is(T == class);
800 }
801 
802 unittest {
803 	static assert(!isClass!int);
804 	static assert( isClass!Object);
805 }
806 
807 template isNotInTypeSet(T, R...) {
808 	import std.meta : staticIndexOf;
809 	enum isNotInTypeSet = staticIndexOf!(T, R) == -1;
810 }
811 
812 string getTypename(Schema,T)(auto ref T input) @trusted {
813 	//pragma(msg, T);
814 	//writefln("To %s", T.stringof);
815 	static if(!isClass!(T)) {
816 		return T.stringof;
817 	} else {
818 		// fetch the typeinfo of the item, and compare it down until we get to a
819 		// class we have. If none found, return the name of the type itself.
820 		import graphql.reflection;
821 		auto tinfo = typeid(input);
822 		const auto reflect = SchemaReflection!Schema.instance;
823 		while(tinfo !is null) {
824 			if(auto cname = tinfo in reflect.classes) {
825 				return *cname;
826 			}
827 			tinfo = tinfo.base;
828 		}
829 		return T.stringof;
830 	}
831 }
832 
833 Json toGraphqlJson(Schema,T)(auto ref T input) {
834 	import std.array : empty;
835 	import std.conv : to;
836 	import std.typecons : Nullable;
837 	import std.traits : isArray, isAggregateType, isBasicType, isSomeString,
838 		   isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple;
839 
840 	import nullablestore;
841 
842 	static if(isArray!T && !isSomeString!T) {
843 		Json ret = Json.emptyArray();
844 		foreach(ref it; input) {
845 			ret ~= toGraphqlJson!Schema(it);
846 		}
847 		return ret;
848 	} else static if(is(T : GQLDCustomLeaf!Type, Type...)) {
849 		return Json(Type[1](input));
850 	} else static if(is(T : Nullable!Type, Type)) {
851 		return input.isNull() ? Json(null) : toGraphqlJson!Schema(input.get());
852 	} else static if(is(T == enum)) {
853 		return Json(to!string(input));
854 	} else static if(isBasicType!T || isScalarType!T || isSomeString!T) {
855 		return serializeToJson(input);
856 	} else static if(isAggregateType!T) {
857 		Json ret = Json.emptyObject();
858 
859 		// the important bit is the setting of the __typename field
860 		ret["__typename"] = getTypename!(Schema)(input);
861 		//writefln("Got %s", ret["__typename"].to!string());
862 
863 		alias names = FieldNameTuple!(T);
864 		alias types = FieldTypeTuple!(T);
865 		static foreach(idx; 0 .. names.length) {{
866 			static if(!names[idx].empty
867 					&& getUdaData!(T, names[idx]).ignore != Ignore.yes
868 					&& !is(types[idx] : NullableStore!Type, Type))
869 			{
870 				static if(is(types[idx] == enum)) {
871 					ret[names[idx]] =
872 						to!string(__traits(getMember, input, names[idx]));
873 				} else {
874 					ret[names[idx]] = toGraphqlJson!Schema(
875 							__traits(getMember, input, names[idx])
876 						);
877 				}
878 			}
879 		}}
880 		return ret;
881 	} else {
882 		static assert(false, T.stringof ~ " not supported");
883 	}
884 }
885 
886 string dtToString(DateTime dt) {
887 	return dt.toISOExtString();
888 }
889 
890 DateTime stringToDT(string s) {
891 	return DateTime.fromISOExtString(s);
892 }
893 
894 string dToString(Date dt) {
895 	return dt.toISOExtString();
896 }
897 
898 unittest {
899 	import std.typecons : nullable, Nullable;
900 	import nullablestore;
901 
902 	struct Foo {
903 		int a;
904 		Nullable!int b;
905 		NullableStore!float c;
906 		GQLDCustomLeaf!(DateTime, dtToString, stringToDT) dt2;
907 		Nullable!(GQLDCustomLeaf!(DateTime, dtToString, stringToDT)) dt;
908 	}
909 
910 	DateTime dt = DateTime(1337, 7, 1, 1, 1, 1);
911 	DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3);
912 
913 	alias DT = GQLDCustomLeaf!(DateTime, dtToString, stringToDT);
914 
915 	Foo foo;
916 	foo.dt2 = DT(dt2);
917 	foo.dt = nullable(DT(dt));
918 	Json j = toGraphqlJson!int(foo);
919 	assert(j["a"].to!int() == 0);
920 	assert(j["b"].type == Json.Type.null_);
921 	assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type,
922 				j.toPrettyString()
923 			)
924 		);
925 	immutable string exp = j["dt"].to!string();
926 	assert(exp == "1337-07-01T01:01:01", exp);
927 	immutable string exp2 = j["dt2"].to!string();
928 	assert(exp2 == "2337-07-01T01:01:03", exp2);
929 
930 	immutable DT back = extract!DT(j, "dt");
931 	assert(back.value == dt);
932 
933 	immutable DT back2 = extract!DT(j, "dt2");
934 	assert(back2.value == dt2);
935 }
936 
937 struct PathElement {
938 	string str;
939 	size_t idx;
940 
941 	static PathElement opCall(string s) {
942 		PathElement ret;
943 		ret.str = s;
944 		return ret;
945 	}
946 
947 	static PathElement opCall(size_t s) {
948 		PathElement ret;
949 		ret.idx = s;
950 		return ret;
951 	}
952 
953 	Json toJson() {
954 		return this.str.empty ? Json(this.idx) : Json(this.str);
955 	}
956 }
957 
958 struct JsonCompareResult {
959 	bool okay;
960 	string[] path;
961 	string message;
962 }
963 
964 JsonCompareResult compareJson(Json a, Json b, string path
965 		, bool allowArrayReorder)
966 {
967 	import std.algorithm.comparison : min;
968 	import std.algorithm.setops : setDifference;
969 	import std.algorithm.sorting : sort;
970 	import std.math : isClose;
971 
972 	if(a.type != b.type) {
973 		return JsonCompareResult(false, [path], format("a.type %s != b.type %s"
974 					, a.type, b.type));
975 	}
976 
977 	if(a.type == Json.Type.array) {
978 		Json[] aArray = a.get!(Json[])();
979 		Json[] bArray = b.get!(Json[])();
980 
981 		size_t minLength = min(aArray.length, bArray.length);
982 		if(allowArrayReorder) {
983 			outer: foreach(idx, it; aArray) {
984 				foreach(jt; bArray) {
985 					JsonCompareResult idxRslt = compareJson(it, jt
986 							, format("[%s]", idx), allowArrayReorder);
987 					if(idxRslt.okay) {
988 						continue outer;
989 					}
990 				}
991 				return JsonCompareResult(false, [ format("[%s]", idx) ]
992 						, "No array element of 'b' matches");
993 			}
994 		} else {
995 			foreach(idx; 0 .. minLength) {
996 				JsonCompareResult idxRslt = compareJson(aArray[idx]
997 						, bArray[idx], format("[%s]", idx), allowArrayReorder);
998 				if(!idxRslt.okay) {
999 					return JsonCompareResult(false, [path] ~ idxRslt.path,
1000 							idxRslt.message);
1001 				}
1002 			}
1003 		}
1004 
1005 		if(aArray.length != bArray.length) {
1006 			return JsonCompareResult(false, [path]
1007 					, format("a.length %s != b.length %s", aArray.length
1008 						, bArray.length));
1009 		}
1010 
1011 		return JsonCompareResult(true, [path], "");
1012 	} else if(a.type == Json.Type.object) {
1013 		Json[string] aObj = a.get!(Json[string])();
1014 		Json[string] bObj = b.get!(Json[string])();
1015 
1016 		foreach(key, value; aObj) {
1017 			Json* bVal = key in bObj;
1018 			if(bVal is null) {
1019 				return JsonCompareResult(false, [path]
1020 						, format("a[\"%s\"] not in b", key));
1021 			} else {
1022 				JsonCompareResult keyRslt = compareJson(value
1023 						, *bVal, format("[\"%s\"]", key), allowArrayReorder);
1024 				if(!keyRslt.okay) {
1025 					return JsonCompareResult(false, [path] ~ keyRslt.path,
1026 							keyRslt.message);
1027 				}
1028 			}
1029 		}
1030 		auto aKeys = aObj.keys.sort;
1031 		auto bKeys = bObj.keys.sort;
1032 
1033 		auto aMinusB = setDifference(aKeys, bKeys);
1034 		auto bMinusA = setDifference(bKeys, aKeys);
1035 
1036 		if(!aMinusB.empty && !bMinusA.empty) {
1037 			return JsonCompareResult(false, [path]
1038 					, format("keys present in 'a' but not in 'b' %s, keys "
1039 						~ "present in 'b' but not in 'a' %s", aMinusB
1040 						, bMinusA));
1041 		} else if(aMinusB.empty && !bMinusA.empty) {
1042 			return JsonCompareResult(false, [path]
1043 					, format("keys present in 'b' but not in 'a' %s", bMinusA));
1044 		} else if(!aMinusB.empty && bMinusA.empty) {
1045 			return JsonCompareResult(false, [path]
1046 					, format("keys present in 'a' but not in 'b' %s", aMinusB));
1047 		}
1048 		return JsonCompareResult(true, [path], "");
1049 	} else if(a.type == Json.Type.Bool) {
1050 		const aBool = a.get!bool();
1051 		const bBool = b.get!bool();
1052 		return JsonCompareResult(aBool == bBool, [path], format("%s != %s", aBool
1053 					, bBool));
1054 	} else if(a.type == Json.Type.Int) {
1055 		const aLong = a.get!long();
1056 		const bLong = b.get!long();
1057 		return JsonCompareResult(aLong == bLong, [path], format("%s != %s", aLong
1058 					, bLong));
1059 	} else if(a.type == Json.Type..string) {
1060 		const aStr = a.get!string();
1061 		const bStr = b.get!string();
1062 		return JsonCompareResult(aStr == bStr, [path], format("%s != %s", aStr
1063 					, bStr));
1064 	} else if(a.type == Json.Type.Float) {
1065 		const aFloat = a.get!double();
1066 		const bFloat = b.get!double();
1067 		return JsonCompareResult(isClose(aFloat, bFloat), [path]
1068 				, format("%s != %s", aFloat, bFloat));
1069 	}
1070 	return JsonCompareResult(true, [path], "");
1071 }