1 module graphql.directives;
2 
3 import std.format : format;
4 
5 import vibe.data.json;
6 
7 import graphql.ast;
8 import graphql.helper;
9 import graphql.astselector;
10 
11 @safe:
12 
13 private enum Include {
14 	undefined = 0,
15 	yes = 1,
16 	no = 4
17 }
18 
19 private enum Skip {
20 	undefined = 0,
21 	yes = 1,
22 	no = 4
23 }
24 
25 T add(T)(T a, T b) {
26 	int ai = a;
27 	int bi = b;
28 	int r = ai + bi;
29 	if(r == 0) {
30 		return cast(T)r;
31 	} else if(r == 1 || r == 2) {
32 		return T.yes;
33 	} else if(r == 4 || r == 8) {
34 		return T.no;
35 	} else {
36 		throw new Exception(format("join conflict between '%s' and '%s'",
37 				a, b));
38 	}
39 }
40 
41 struct SkipInclude {
42 	Include include;
43 	Skip skip;
44 
45 	SkipInclude join(SkipInclude other) {
46 		SkipInclude ret = this;
47 		ret.include = add(ret.include, other.include);
48 		ret.skip = add(ret.skip, other.skip);
49 		return ret;
50 	}
51 }
52 
53 bool continueAfterDirectives(const(Directives) dirs, Json vars) {
54 	import graphql.validation.exception;
55 
56 	SkipInclude si = extractSkipInclude(dirs, vars);
57 	if(si.include == Include.undefined && si.skip == Skip.undefined) {
58 		return true;
59 	} else if(si.include == Include.no && si.skip == Skip.undefined) {
60 		return false;
61 	} else if(si.include == Include.yes
62 			&& (si.skip == Skip.undefined || si.skip == Skip.no))
63 	{
64 		return true;
65 	} else if(si.include == Include.undefined && si.skip == Skip.no) {
66 		return true;
67 	} else if(si.include == Include.undefined && si.skip == Skip.yes) {
68 		return false;
69 	}
70 	throw new ContradictingDirectives(format(
71 			"include %s and skip %s contridict each other", si.include,
72 			si.skip), __FILE__, __LINE__);
73 }
74 
75 SkipInclude extractSkipInclude(const(Directives) dirs, Json vars) {
76 	import graphql.argumentextractor;
77 
78 	SkipInclude ret;
79 	if(dirs is null) {
80 		return ret;
81 	}
82 	Json args = getArguments(dirs.dir, vars);
83 	bool if_ = args.extract!bool("if");
84 	ret.include = dirs.dir.name.value == "include"
85 		? if_
86 			? Include.yes
87 			: Include.no
88 		: Include.undefined;
89 	ret.skip = dirs.dir.name.value == "skip"
90 		? if_
91 			? Skip.yes
92 			: Skip.no
93 		: Skip.undefined;
94 
95 	return ret.join(extractSkipInclude(dirs.follow, vars));
96 }
97 
98 unittest {
99 	string q = `
100 query a($s: boolean) {
101 	starships(overSize: 10) {
102 		name @include(if: $s)
103 		crew @skip(if: $s) {
104 		id
105 		}
106 	}
107 }`;
108 
109 	Json vars = parseJsonString(`{ "s": false }`);
110 
111 	const(Document) doc = lexAndParse(q);
112 	assert(doc !is null);
113 	{
114 		auto name = astSelect!Field(doc, "a.starships.name");
115 		assert(name !is null);
116 
117 		SkipInclude si = extractSkipInclude(name.dirs, vars);
118 		assert(si.include == Include.no, format("%s", si.include));
119 		assert(si.skip == Skip.undefined, format("%s", si.skip));
120 		assert(!continueAfterDirectives(name.dirs, vars));
121 	}
122 	{
123 		auto crew = astSelect!Field(doc, "a.starships.crew");
124 		assert(crew !is null);
125 
126 		SkipInclude si = extractSkipInclude(crew.dirs, vars);
127 		assert(si.include == Include.undefined);
128 		assert(si.skip == Skip.no);
129 		assert(continueAfterDirectives(crew.dirs, vars));
130 	}
131 }