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 					enforce(ap.type == value.type && *ap == value
252 						, format("Can not join '%s' and '%s' on key '%s'"
253 							, ap.type, value.type, key)
254 					);
255 				} else static if(jjp == JoinJsonPrecedence.a) {
256 				} else {
257 					ret[key] = value;
258 				}
259 			}
260 		}
261 		return ret;
262 	}
263 	return a;
264 }
265 
266 unittest {
267 	Json a = parseJsonString(`{"overSize":200}`);
268 	Json b = parseJsonString(`{}`);
269 	const c = joinJson(b, a);
270 	assert(c == a);
271 
272 	b = parseJsonString(`{"underSize":-100}`);
273 	const d = joinJson(b, a);
274 	immutable Json r = parseJsonString(`{"overSize":200, "underSize":-100}`);
275 	assert(d == r);
276 }
277 
278 unittest {
279 	Json j = joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
280 			parseJsonString(`{"underSize": {"b": 100}}`)
281 		);
282 
283 	Json r = parseJsonString(`{"underSize": {"a": -100, "b": 100}}`);
284 	assert(j == r, format("%s\n\n%s", j.toPrettyString(), r.toPrettyString()));
285 }
286 
287 unittest {
288 	assertThrown(joinJson(parseJsonString(`{"underSize": {"a": -100}}`),
289 			parseJsonString(`{"underSize": {"a": 100}}`)
290 		));
291 }
292 
293 unittest {
294 	assertThrown(joinJson(parseJsonString(`{"underSize": -100}`),
295 			parseJsonString(`{"underSize": {"a": 100}}`)
296 		));
297 }
298 
299 template toType(T) {
300 	import std.bigint : BigInt;
301 	import std.traits : isArray, isIntegral, isAggregateType, isFloatingPoint,
302 		   isSomeString;
303 	static if(is(T == bool)) {
304 		enum toType = Json.Type.bool_;
305 	} else static if(isIntegral!(T)) {
306 		enum toType = Json.Type.int_;
307 	} else static if(isFloatingPoint!(T)) {
308 		enum toType = Json.Type.float_;
309 	} else static if(isSomeString!(T)) {
310 		enum toType = Json.Type..string;
311 	} else static if(isArray!(T)) {
312 		enum toType = Json.Type.array;
313 	} else static if(isAggregateType!(T)) {
314 		enum toType = Json.Type.object;
315 	} else static if(is(T == BigInt)) {
316 		enum toType = Json.Type.bigint;
317 	} else {
318 		enum toType = Json.Type.undefined;
319 	}
320 }
321 
322 bool hasPathTo(T)(Json data, string path, ref T ret) {
323 	enum TT = toType!T;
324 	auto sp = path.splitter(".");
325 	string f;
326 	while(!sp.empty) {
327 		f = sp.front;
328 		sp.popFront();
329 		if(data.type != Json.Type.object || f !in data) {
330 			return false;
331 		} else {
332 			data = data[f];
333 		}
334 	}
335 	static if(is(T == Json)) {
336 		ret = data;
337 		return true;
338 	} else {
339 		if(data.type == TT) {
340 			ret = data.to!T();
341 			return true;
342 		}
343 		return false;
344 	}
345 }
346 
347 unittest {
348 	Json d = parseJsonString(`{ "foo" : { "path" : "foo" } }`);
349 	Json ret;
350 	assert(hasPathTo!Json(d, "foo", ret));
351 	assert("path" in ret);
352 	assert(ret["path"].type == Json.Type..string);
353 	assert(ret["path"].get!string() == "foo");
354 }
355 
356 /**
357 params:
358 	path = A "." seperated path
359 */
360 T getWithDefault(T)(Json data, string[] paths...) {
361 	enum TT = toType!T;
362 	T ret = T.init;
363 	foreach(string path; paths) {
364 		if(hasPathTo!T(data, path, ret)) {
365 			return ret;
366 		}
367 	}
368 	return ret;
369 }
370 
371 unittest {
372 	Json d = parseJsonString(`{"errors":[],"data":{"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("data.__typename");
377 	assert(r == "Starship", r);
378 }
379 
380 unittest {
381 	Json d = parseJsonString(`{"commanderId":8,
382 			"__typename":"Starship","series":["DeepSpaceNine",
383 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
384 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
385 	const r = d.getWithDefault!string("data.__typename", "__typename");
386 	assert(r == "Starship", r);
387 }
388 
389 unittest {
390 	Json d = parseJsonString(`{"commanderId":8,
391 			"__typename":"Starship","series":["DeepSpaceNine",
392 			"TheOriginalSeries"],"id":43,"name":"Defiant","size":130,
393 			"crewIds":[9,10,11,1,12,13,8],"designation":"NX-74205"}`);
394 	const r = d.getWithDefault!string("__typename");
395 	assert(r == "Starship", r);
396 }
397 
398 // TODO should return ref
399 auto accessNN(string[] tokens,T)(T tmp0) {
400 	import std.array : back;
401 	import std.format : format;
402 	if(tmp0 !is null) {
403 		static foreach(idx, token; tokens) {
404 			mixin(format(
405 				`if(tmp%d is null) return null;
406 				auto tmp%d = tmp%d.%s;`, idx, idx+1, idx, token)
407 			);
408 		}
409 		return mixin(format("tmp%d", tokens.length));
410 	}
411 	return null;
412 }
413 
414 unittest {
415 	class A {
416 		int a;
417 	}
418 
419 	class B {
420 		A a;
421 	}
422 
423 	class C {
424 		B b;
425 	}
426 
427 	auto c1 = new C;
428 	assert(c1.accessNN!(["b", "a"]) is null);
429 
430 	c1.b = new B;
431 	assert(c1.accessNN!(["b"]) !is null);
432 
433 	assert(c1.accessNN!(["b", "a"]) is null);
434 	// TODO not sure why this is not a lvalue
435 	//c1.accessNN!(["b", "a"]) = new A;
436 	c1.b.a = new A;
437 	assert(c1.accessNN!(["b", "a"]) !is null);
438 }
439 
440 T jsonTo(T)(Json item) {
441 	static import std.conv;
442 	static if(is(T == enum)) {
443 		enforce!GQLDExecutionException(item.type == Json.Type..string,
444 			format("Enum '%s' must be passed as string not '%s'",
445 				T.stringof, item.type));
446 
447 		string s = item.to!string();
448 		try {
449 			return std.conv.to!T(s);
450 		} catch(Exception c) {
451 			throw new GQLDExecutionException(c.msg);
452 		}
453 	} else static if(is(T == GQLDCustomLeaf!Fs, Fs...)) {
454 		enforce!GQLDExecutionException(item.type == Json.Type..string,
455 			format("%1$s '%1$s' must be passed as string not '%2$s'",
456 				T.stringof, item.type));
457 
458 		string s = item.to!string();
459 		try {
460 			return T(Fs[2](s));
461 		} catch(Exception c) {
462 			throw new GQLDExecutionException(c.msg);
463 		}
464 	} else {
465 		try {
466 			return item.to!T();
467 		} catch(Exception c) {
468 			throw new GQLDExecutionException(c.msg);
469 		}
470 	}
471 }
472 
473 T extract(T)(Json data, string name) {
474 	enforce!GQLDExecutionException(data.type == Json.Type.object, format(
475 			"Trying to get a '%s' by name '%s' but passed Json is not an object"
476 			, T.stringof, name)
477 		);
478 
479 	Json* item = name in data;
480 
481 	enforce!GQLDExecutionException(item !is null, format(
482 			"Trying to get a '%s' by name '%s' which is not present in passed "
483 			~ "object '%s'"
484 			, T.stringof, name, data)
485 		);
486 
487 	return jsonTo!(T)(*item);
488 }
489 
490 unittest {
491 	import std.exception : assertThrown;
492 	Json j = parseJsonString(`null`);
493 	assertThrown(j.extract!string("Hello"));
494 }
495 
496 unittest {
497 	enum E {
498 		no,
499 		yes
500 	}
501 	import std.exception : assertThrown;
502 	Json j = parseJsonString(`{ "foo": 1337 }`);
503 
504 	assertThrown(j.extract!E("foo"));
505 
506 	j = parseJsonString(`{ "foo": "str" }`);
507 	assertThrown(j.extract!E("foo"));
508 
509 	j = parseJsonString(`{ "foo": "yes" }`);
510 	assert(j.extract!E("foo") == E.yes);
511 }
512 
513 unittest {
514 	import std.exception : assertThrown;
515 	Json j = parseJsonString(`{ "foo": 1337 }`);
516 	immutable auto foo = j.extract!int("foo");
517 
518 	assertThrown(Json.emptyObject().extract!float("Hello"));
519 	assertThrown(j.extract!string("Hello"));
520 }
521 
522 unittest {
523 	import std.exception : assertThrown;
524 	enum FooEn {
525 		a,
526 		b
527 	}
528 	Json j = parseJsonString(`{ "foo": "a" }`);
529 	immutable auto foo = j.extract!FooEn("foo");
530 	assert(foo == FooEn.a);
531 
532 	assertThrown(Json.emptyObject().extract!float("Hello"));
533 	assertThrown(j.extract!string("Hello"));
534 	assert(j["foo"].jsonTo!FooEn() == FooEn.a);
535 
536 	Json k = parseJsonString(`{ "foo": "b" }`);
537 	assert(k["foo"].jsonTo!FooEn() == FooEn.b);
538 }
539 
540 const(Document) lexAndParse(string s) {
541 	import graphql.lexer;
542 	import graphql.parser;
543 	auto l = Lexer(s, QueryParser.no);
544 	auto p = Parser(l);
545 	const(Document) doc = p.parseDocument();
546 	return doc;
547 }
548 
549 struct StringTypeStrip {
550 	string input;
551 	string str;
552 	bool outerNotNull;
553 	bool arr;
554 	bool innerNotNull;
555 
556 	string toString() const {
557 		import std.format : format;
558 		return format("StringTypeStrip(input:'%s', str:'%s', "
559 			   ~ "arr:'%s', outerNotNull:'%s', innerNotNull:'%s')",
560 			   this.input, this.str, this.arr, this.outerNotNull,
561 			   this.innerNotNull);
562 	}
563 }
564 
565 StringTypeStrip stringTypeStrip(string str) {
566 	Nullable!StringTypeStrip gqld = gqldStringTypeStrip(str);
567 	return gqld.get();
568 	//return gqld.isNull()
569 	//	? dlangStringTypeStrip(str)
570 	//	: gqld.get();
571 }
572 
573 private Nullable!StringTypeStrip gqldStringTypeStrip(string str) {
574 	StringTypeStrip ret;
575 	ret.input = str;
576 	immutable string old = str;
577 	bool firstBang;
578 	if(str.endsWith('!')) {
579 		firstBang = true;
580 		str = str[0 .. $ - 1];
581 	}
582 
583 	bool arr;
584 	if(str.startsWith('[') && str.endsWith(']')) {
585 		arr = true;
586 		str = str[1 .. $ - 1];
587 	}
588 
589 	bool secondBang;
590 	if(str.endsWith('!')) {
591 		secondBang = true;
592 		str = str[0 .. $ - 1];
593 	}
594 
595 	if(arr) {
596 		ret.innerNotNull = secondBang;
597 		ret.outerNotNull = firstBang;
598 	} else {
599 		ret.innerNotNull = firstBang;
600 	}
601 
602 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
603 		? "Int"
604 		: str;
605 
606 	str = canFind(["string", "int", "float", "bool"], str)
607 		? capitalize(str)
608 		: str;
609 
610 	str = str == "__type" ? "__Type" : str;
611 	str = str == "__schema" ? "__Schema" : str;
612 	str = str == "__inputvalue" ? "__InputValue" : str;
613 	str = str == "__directive" ? "__Directive" : str;
614 	str = str == "__field" ? "__Field" : str;
615 
616 	ret.arr = arr;
617 
618 	ret.str = str;
619 	//writefln("%s %s", __LINE__, ret);
620 
621 	//return old == str ? Nullable!(StringTypeStrip).init : nullable(ret);
622 	return nullable(ret);
623 }
624 
625 unittest {
626 	auto a = gqldStringTypeStrip("String");
627 	assert(!a.isNull());
628 
629 	a = gqldStringTypeStrip("String!");
630 	assert(!a.isNull());
631 	assert(a.get().str == "String");
632 	assert(a.get().innerNotNull, format("%s", a.get()));
633 
634 	a = gqldStringTypeStrip("[String!]");
635 	assert(!a.isNull());
636 	assert(a.get().str == "String");
637 	assert(a.get().arr, format("%s", a.get()));
638 	assert(a.get().innerNotNull, format("%s", a.get()));
639 
640 	a = gqldStringTypeStrip("[String]!");
641 	assert(!a.isNull());
642 	assert(a.get().str == "String");
643 	assert(a.get().arr, format("%s", a.get()));
644 	assert(!a.get().innerNotNull, format("%s", a.get()));
645 	assert(a.get().outerNotNull, format("%s", a.get()));
646 
647 	a = gqldStringTypeStrip("[String!]!");
648 	assert(!a.isNull());
649 	assert(a.get().str == "String");
650 	assert(a.get().arr, format("%s", a.get()));
651 	assert(a.get().innerNotNull, format("%s", a.get()));
652 	assert(a.get().outerNotNull, format("%s", a.get()));
653 }
654 
655 private StringTypeStrip dlangStringTypeStrip(string str) {
656 	StringTypeStrip ret;
657 	ret.outerNotNull = true;
658 	ret.innerNotNull = true;
659 	ret.input = str;
660 
661 	immutable ns = "NullableStore!";
662 	immutable ns1 = "NullableStore!(";
663 	immutable leaf = "GQLDCustomLeaf!";
664 	immutable leaf1 = "GQLDCustomLeaf!(";
665 	immutable nll = "Nullable!";
666 	immutable nll1 = "Nullable!(";
667 
668 	// NullableStore!( .... )
669 	if(str.startsWith(ns1) && str.endsWith(")")) {
670 		str = str[ns1.length .. $ - 1];
671 	}
672 
673 	// NullableStore!....
674 	if(str.startsWith(ns)) {
675 		str = str[ns.length .. $];
676 	}
677 
678 	// GQLDCustomLeaf!( .... )
679 	if(str.startsWith(leaf1) && str.endsWith(")")) {
680 		str = str[leaf1.length .. $ - 1];
681 	}
682 
683 	bool firstNull;
684 
685 	// Nullable!( .... )
686 	if(str.startsWith(nll1) && str.endsWith(")")) {
687 		firstNull = true;
688 		str = str[nll1.length .. $ - 1];
689 	}
690 
691 	// NullableStore!( .... )
692 	if(str.startsWith(ns1) && str.endsWith(")")) {
693 		str = str[ns1.length .. $ - 1];
694 	}
695 
696 	// NullableStore!....
697 	if(str.startsWith(ns)) {
698 		str = str[ns.length .. $];
699 	}
700 
701 	if(str.endsWith("!")) {
702 		str = str[0 .. $ - 1];
703 	}
704 
705 	// xxxxxxx[]
706 	if(str.endsWith("[]")) {
707 		ret.arr = true;
708 		str = str[0 .. $ - 2];
709 	}
710 
711 	bool secondNull;
712 
713 	// Nullable!( .... )
714 	if(str.startsWith(nll1) && str.endsWith(")")) {
715 		secondNull = true;
716 		str = str[nll1.length .. $ - 1];
717 	}
718 
719 	if(str.endsWith("!")) {
720 		str = str[0 .. $ - 1];
721 	}
722 
723 	// Nullable! ....
724 	if(str.startsWith(nll)) {
725 		secondNull = true;
726 		str = str[nll.length .. $];
727 	}
728 
729 	// NullableStore!( .... )
730 	if(str.startsWith(ns1) && str.endsWith(")")) {
731 		str = str[ns1.length .. $ - 1];
732 	}
733 
734 	// NullableStore!....
735 	if(str.startsWith(ns)) {
736 		str = str[ns.length .. $];
737 	}
738 
739 	str = canFind(["ubyte", "byte", "ushort", "short", "long", "ulong"], str)
740 		? "Int"
741 		: str;
742 
743 	str = canFind(["string", "int", "float", "bool"], str)
744 		? capitalize(str)
745 		: str;
746 
747 	str = str == "__type" ? "__Type" : str;
748 	str = str == "__schema" ? "__Schema" : str;
749 	str = str == "__inputvalue" ? "__InputValue" : str;
750 	str = str == "__directive" ? "__Directive" : str;
751 	str = str == "__field" ? "__Field" : str;
752 
753 	//writefln("firstNull %s, secondNull %s, arr %s", firstNull, secondNull,
754 	//		ret.arr);
755 
756 	if(ret.arr) {
757 		ret.innerNotNull = !secondNull;
758 		ret.outerNotNull = !firstNull;
759 	} else {
760 		ret.innerNotNull = !secondNull;
761 	}
762 
763 	ret.str = str;
764 	return ret;
765 }
766 
767 unittest {
768 	string t = "Nullable!string";
769 	StringTypeStrip r = t.dlangStringTypeStrip();
770 	assert(r.str == "String", to!string(r));
771 	assert(!r.arr, to!string(r));
772 	assert(!r.innerNotNull, to!string(r));
773 	assert(r.outerNotNull, to!string(r));
774 
775 	t = "Nullable!(string[])";
776 	r = t.dlangStringTypeStrip();
777 	assert(r.str == "String", to!string(r));
778 	assert(r.arr, to!string(r));
779 	assert(r.innerNotNull, to!string(r));
780 	assert(!r.outerNotNull, to!string(r));
781 }
782 
783 unittest {
784 	string t = "Nullable!__type";
785 	StringTypeStrip r = t.dlangStringTypeStrip();
786 	assert(r.str == "__Type", to!string(r));
787 	assert(!r.innerNotNull, to!string(r));
788 	assert(r.outerNotNull, to!string(r));
789 	assert(!r.arr, to!string(r));
790 
791 	t = "Nullable!(__type[])";
792 	r = t.dlangStringTypeStrip();
793 	assert(r.str == "__Type", to!string(r));
794 	assert(r.innerNotNull, to!string(r));
795 	assert(!r.outerNotNull, to!string(r));
796 	assert(r.arr, to!string(r));
797 }
798 
799 template isClass(T) {
800 	enum isClass = is(T == class);
801 }
802 
803 unittest {
804 	static assert(!isClass!int);
805 	static assert( isClass!Object);
806 }
807 
808 template isNotInTypeSet(T, R...) {
809 	import std.meta : staticIndexOf;
810 	enum isNotInTypeSet = staticIndexOf!(T, R) == -1;
811 }
812 
813 string getTypename(Schema,T)(auto ref T input) @trusted {
814 	//pragma(msg, T);
815 	//writefln("To %s", T.stringof);
816 	static if(!isClass!(T)) {
817 		return T.stringof;
818 	} else {
819 		// fetch the typeinfo of the item, and compare it down until we get to a
820 		// class we have. If none found, return the name of the type itself.
821 		import graphql.reflection;
822 		auto tinfo = typeid(input);
823 		const auto reflect = SchemaReflection!Schema.instance;
824 		while(tinfo !is null) {
825 			if(auto cname = tinfo in reflect.classes) {
826 				return *cname;
827 			}
828 			tinfo = tinfo.base;
829 		}
830 		return T.stringof;
831 	}
832 }
833 
834 Json toGraphqlJson(Schema,T)(auto ref T input) {
835 	import std.array : empty;
836 	import std.conv : to;
837 	import std.typecons : Nullable;
838 	import std.traits : isArray, isAggregateType, isBasicType, isSomeString,
839 		   isScalarType, isSomeString, FieldNameTuple, FieldTypeTuple;
840 
841 	import nullablestore;
842 
843 	static if(isArray!T && !isSomeString!T) {
844 		Json ret = Json.emptyArray();
845 		foreach(ref it; input) {
846 			ret ~= toGraphqlJson!Schema(it);
847 		}
848 		return ret;
849 	} else static if(is(T : GQLDCustomLeaf!Type, Type...)) {
850 		return Json(Type[1](input));
851 	} else static if(is(T : Nullable!Type, Type)) {
852 		return input.isNull() ? Json(null) : toGraphqlJson!Schema(input.get());
853 	} else static if(is(T == enum)) {
854 		return Json(to!string(input));
855 	} else static if(isBasicType!T || isScalarType!T || isSomeString!T) {
856 		return serializeToJson(input);
857 	} else static if(isAggregateType!T) {
858 		Json ret = Json.emptyObject();
859 
860 		// the important bit is the setting of the __typename field
861 		ret["__typename"] = getTypename!(Schema)(input);
862 		//writefln("Got %s", ret["__typename"].to!string());
863 
864 		alias names = FieldNameTuple!(T);
865 		alias types = FieldTypeTuple!(T);
866 		static foreach(idx; 0 .. names.length) {{
867 			static if(!names[idx].empty
868 					&& getUdaData!(T, names[idx]).ignore != Ignore.yes
869 					&& !is(types[idx] : NullableStore!Type, Type))
870 			{
871 				static if(is(types[idx] == enum)) {
872 					ret[names[idx]] =
873 						to!string(__traits(getMember, input, names[idx]));
874 				} else {
875 					ret[names[idx]] = toGraphqlJson!Schema(
876 							__traits(getMember, input, names[idx])
877 						);
878 				}
879 			}
880 		}}
881 		return ret;
882 	} else {
883 		static assert(false, T.stringof ~ " not supported");
884 	}
885 }
886 
887 string dtToString(DateTime dt) {
888 	return dt.toISOExtString();
889 }
890 
891 DateTime stringToDT(string s) {
892 	return DateTime.fromISOExtString(s);
893 }
894 
895 string dToString(Date dt) {
896 	return dt.toISOExtString();
897 }
898 
899 unittest {
900 	import std.typecons : nullable, Nullable;
901 	import nullablestore;
902 
903 	struct Foo {
904 		int a;
905 		Nullable!int b;
906 		NullableStore!float c;
907 		GQLDCustomLeaf!(DateTime, dtToString, stringToDT) dt2;
908 		Nullable!(GQLDCustomLeaf!(DateTime, dtToString, stringToDT)) dt;
909 	}
910 
911 	DateTime dt = DateTime(1337, 7, 1, 1, 1, 1);
912 	DateTime dt2 = DateTime(2337, 7, 1, 1, 1, 3);
913 
914 	alias DT = GQLDCustomLeaf!(DateTime, dtToString, stringToDT);
915 
916 	Foo foo;
917 	foo.dt2 = DT(dt2);
918 	foo.dt = nullable(DT(dt));
919 	Json j = toGraphqlJson!int(foo);
920 	assert(j["a"].to!int() == 0);
921 	assert(j["b"].type == Json.Type.null_);
922 	assert(j["dt"].type == Json.Type..string, format("%s\n%s", j["dt"].type,
923 				j.toPrettyString()
924 			)
925 		);
926 	immutable string exp = j["dt"].to!string();
927 	assert(exp == "1337-07-01T01:01:01", exp);
928 	immutable string exp2 = j["dt2"].to!string();
929 	assert(exp2 == "2337-07-01T01:01:03", exp2);
930 
931 	immutable DT back = extract!DT(j, "dt");
932 	assert(back.value == dt);
933 
934 	immutable DT back2 = extract!DT(j, "dt2");
935 	assert(back2.value == dt2);
936 }
937 
938 struct PathElement {
939 	string str;
940 	size_t idx;
941 
942 	static PathElement opCall(string s) {
943 		PathElement ret;
944 		ret.str = s;
945 		return ret;
946 	}
947 
948 	static PathElement opCall(size_t s) {
949 		PathElement ret;
950 		ret.idx = s;
951 		return ret;
952 	}
953 
954 	Json toJson() {
955 		return this.str.empty ? Json(this.idx) : Json(this.str);
956 	}
957 }
958 
959 struct JsonCompareResult {
960 	bool okay;
961 	string[] path;
962 	string message;
963 }
964 
965 JsonCompareResult compareJson(Json a, Json b, string path
966 		, bool allowArrayReorder)
967 {
968 	import std.algorithm.comparison : min;
969 	import std.algorithm.setops : setDifference;
970 	import std.algorithm.sorting : sort;
971 	import std.math : isClose;
972 
973 	if(a.type != b.type) {
974 		return JsonCompareResult(false, [path], format("a.type %s != b.type %s"
975 					, a.type, b.type));
976 	}
977 
978 	if(a.type == Json.Type.array) {
979 		Json[] aArray = a.get!(Json[])();
980 		Json[] bArray = b.get!(Json[])();
981 
982 		size_t minLength = min(aArray.length, bArray.length);
983 		if(allowArrayReorder) {
984 			outer: foreach(idx, it; aArray) {
985 				foreach(jt; bArray) {
986 					JsonCompareResult idxRslt = compareJson(it, jt
987 							, format("[%s]", idx), allowArrayReorder);
988 					if(idxRslt.okay) {
989 						continue outer;
990 					}
991 				}
992 				return JsonCompareResult(false, [ format("[%s]", idx) ]
993 						, "No array element of 'b' matches");
994 			}
995 		} else {
996 			foreach(idx; 0 .. minLength) {
997 				JsonCompareResult idxRslt = compareJson(aArray[idx]
998 						, bArray[idx], format("[%s]", idx), allowArrayReorder);
999 				if(!idxRslt.okay) {
1000 					return JsonCompareResult(false, [path] ~ idxRslt.path,
1001 							idxRslt.message);
1002 				}
1003 			}
1004 		}
1005 
1006 		if(aArray.length != bArray.length) {
1007 			return JsonCompareResult(false, [path]
1008 					, format("a.length %s != b.length %s", aArray.length
1009 						, bArray.length));
1010 		}
1011 
1012 		return JsonCompareResult(true, [path], "");
1013 	} else if(a.type == Json.Type.object) {
1014 		Json[string] aObj = a.get!(Json[string])();
1015 		Json[string] bObj = b.get!(Json[string])();
1016 
1017 		foreach(key, value; aObj) {
1018 			Json* bVal = key in bObj;
1019 			if(bVal is null) {
1020 				return JsonCompareResult(false, [path]
1021 						, format("a[\"%s\"] not in b", key));
1022 			} else {
1023 				JsonCompareResult keyRslt = compareJson(value
1024 						, *bVal, format("[\"%s\"]", key), allowArrayReorder);
1025 				if(!keyRslt.okay) {
1026 					return JsonCompareResult(false, [path] ~ keyRslt.path,
1027 							keyRslt.message);
1028 				}
1029 			}
1030 		}
1031 		auto aKeys = aObj.keys.sort;
1032 		auto bKeys = bObj.keys.sort;
1033 
1034 		auto aMinusB = setDifference(aKeys, bKeys);
1035 		auto bMinusA = setDifference(bKeys, aKeys);
1036 
1037 		if(!aMinusB.empty && !bMinusA.empty) {
1038 			return JsonCompareResult(false, [path]
1039 					, format("keys present in 'a' but not in 'b' %s, keys "
1040 						~ "present in 'b' but not in 'a' %s", aMinusB
1041 						, bMinusA));
1042 		} else if(aMinusB.empty && !bMinusA.empty) {
1043 			return JsonCompareResult(false, [path]
1044 					, format("keys present in 'b' but not in 'a' %s", bMinusA));
1045 		} else if(!aMinusB.empty && bMinusA.empty) {
1046 			return JsonCompareResult(false, [path]
1047 					, format("keys present in 'a' but not in 'b' %s", aMinusB));
1048 		}
1049 		return JsonCompareResult(true, [path], "");
1050 	} else if(a.type == Json.Type.Bool) {
1051 		const aBool = a.get!bool();
1052 		const bBool = b.get!bool();
1053 		return JsonCompareResult(aBool == bBool, [path], format("%s != %s", aBool
1054 					, bBool));
1055 	} else if(a.type == Json.Type.Int) {
1056 		const aLong = a.get!long();
1057 		const bLong = b.get!long();
1058 		return JsonCompareResult(aLong == bLong, [path], format("%s != %s", aLong
1059 					, bLong));
1060 	} else if(a.type == Json.Type..string) {
1061 		const aStr = a.get!string();
1062 		const bStr = b.get!string();
1063 		return JsonCompareResult(aStr == bStr, [path], format("%s != %s", aStr
1064 					, bStr));
1065 	} else if(a.type == Json.Type.Float) {
1066 		const aFloat = a.get!double();
1067 		const bFloat = b.get!double();
1068 		return JsonCompareResult(isClose(aFloat, bFloat), [path]
1069 				, format("%s != %s", aFloat, bFloat));
1070 	}
1071 	return JsonCompareResult(true, [path], "");
1072 }