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