1 module graphql.astselector;
2 
3 import std.array : back, empty, front, popBack;
4 import std.exception : enforce;
5 
6 import graphql.tokenmodule;
7 import graphql.ast;
8 import graphql.visitor : ConstVisitor;
9 
10 /* Sometimes you need to select a specific part of the ast,
11 this module allows to do that.
12 */
13 
14 @safe:
15 
16 const(T) astSelect(T,S)(const(S) input, string path) {
17 	auto astsel = new AstSelector(path);
18 	static if(is(S == Document)) {
19 		return astsel.get!T(input, input);
20 	} else {
21 		return astsel.get!T(input);
22 	}
23 }
24 
25 const(T) astSelect(T,S)(const(S) input, const(Document) doc, string path) {
26 	auto astsel = new AstSelector(path);
27 	return astsel.get!T(input, doc);
28 }
29 
30 class AstSelector : ConstVisitor {
31 	import std.format : format;
32 	import std.typecons : rebindable, Rebindable;
33 	alias enter = ConstVisitor.enter;
34 	alias exit = ConstVisitor.exit;
35 	alias accept = ConstVisitor.accept;
36 
37 	Rebindable!(const(Document)) document;
38 
39 	const(string[]) sp;
40 	size_t spPos;
41 
42 	Rebindable!(const(Node)) result;
43 	Rebindable!(const(Node))[] stack;
44 
45 	this(string p) {
46 		import std..string : split;
47 		this.sp = p.split('.');
48 	}
49 
50 	const(T) get(T,S)(const(S) input, const(Document) doc) {
51 		this.document = doc;
52 		this.accept(input);
53 		return cast(typeof(return))this.result.get();
54 	}
55 
56 	const(T) get(T,S)(const(S) input) {
57 		return this.get!T(input, null);
58 	}
59 
60 	bool takeName(string name, const(Node) nn) {
61 		if(this.spPos < this.sp.length && name == this.sp[this.spPos]) {
62 			this.stack ~= rebindable(nn);
63 		} else {
64 			return false;
65 		}
66 
67 		++this.spPos;
68 		if(this.spPos == this.sp.length) {
69 			enforce(this.result.get() is null);
70 			this.result = this.stack.back;
71 		}
72 		return true;
73 	}
74 
75 	void popStack(bool shouldPop) {
76 		enforce(this.stack.length == this.spPos,
77 				format("stack.length %s, spPos %s", this.stack.length,
78 					this.spPos));
79 		enforce(shouldPop ? this.stack.length > 0 : true,
80 				"should pop put stack is empty");
81 		if(shouldPop) {
82 			this.stack.popBack();
83 			--this.spPos;
84 		}
85 	}
86 
87 	override void accept(const(OperationDefinition) obj) {
88 		if(obj.name.type != TokenType.undefined) {
89 			immutable bool shouldPop = this.takeName(obj.name.value, obj);
90 			if(shouldPop) {
91 				if(obj.vd !is null) {
92 					obj.vd.visit(this);
93 				}
94 				if(obj.d !is null) {
95 					obj.d.visit(this);
96 				}
97 				if(obj.ss !is null) {
98 					obj.ss.visit(this);
99 				}
100 			}
101 			this.popStack(shouldPop);
102 		}
103 	}
104 
105 	override void accept(const(Field) obj) {
106 		bool shouldPop;
107 
108 		scope(exit) {
109 			this.popStack(shouldPop);
110 		}
111 
112 		shouldPop = this.takeName(obj.name.name.value, obj);
113 
114 		if(shouldPop) {
115 			if(obj.args !is null) {
116 				obj.args.visit(this);
117 			}
118 			if(obj.dirs !is null) {
119 				obj.dirs.visit(this);
120 			}
121 			if(obj.ss !is null) {
122 				obj.ss.visit(this);
123 			}
124 		}
125 	}
126 
127 	override void accept(const(InlineFragment) obj) {
128 		if(obj.tc.type != TokenType.undefined) {
129 			immutable bool shouldPop = this.takeName(obj.tc.value, obj);
130 			if(shouldPop) {
131 				if(obj.dirs !is null) {
132 					obj.dirs.visit(this);
133 				}
134 				if(obj.ss !is null) {
135 					obj.ss.visit(this);
136 				}
137 			}
138 			this.popStack(shouldPop);
139 		}
140 	}
141 
142 	override void accept(const(FragmentSpread) fragSpread) {
143 		import graphql.builder : findFragment;
144 		const(FragmentDefinition) frag = findFragment(this.document.get(),
145 				fragSpread.name.value
146 			);
147 		immutable bool shouldPop = this.takeName(fragSpread.name.value, frag);
148 		frag.visit(this);
149 		this.popStack(shouldPop);
150 	}
151 }
152 
153 unittest {
154 	string s = `
155 query foo {
156 	a
157 }`;
158 
159 	auto d = lexAndParse(s);
160 	auto foo = d.astSelect!OperationDefinition("foo");
161 	assert(foo !is null);
162 	assert(foo.name.value == "foo");
163 }
164 
165 unittest {
166 	string s = `
167 query foo {
168 	a
169 }
170 
171 mutation bar {
172 	b
173 }
174 
175 `;
176 
177 	auto d = lexAndParse(s);
178 	auto bar = d.astSelect!OperationDefinition("bar");
179 	assert(bar !is null);
180 	assert(bar.name.value == "bar");
181 }
182 
183 unittest {
184 	string s = `
185 query foo {
186 	a {
187 		b
188 	}
189 }
190 
191 `;
192 
193 	auto d = lexAndParse(s);
194 	auto a = d.astSelect!Field("foo.a");
195 	assert(a !is null);
196 	assert(a.name.name.value == "a");
197 }
198 
199 unittest {
200 	string s = `
201 query foo {
202 	a {
203 		b
204 	}
205 }
206 
207 `;
208 
209 	auto d = lexAndParse(s);
210 	auto a = d.astSelect!Document("foo.a");
211 	assert(a is null);
212 }
213 
214 unittest {
215 	string s = `
216 query foo {
217 	a {
218 		b
219 	}
220 }
221 
222 mutation a {
223 	foo {
224 		b
225 	}
226 }
227 
228 `;
229 
230 	auto d = lexAndParse(s);
231 	auto foo = d.astSelect!Field("a.foo");
232 	assert(foo !is null);
233 }
234 
235 unittest {
236 	string s = `
237 query foo {
238 	a {
239 		b
240 	}
241 	c {
242 		b
243 	}
244 }
245 
246 mutation a {
247 	foo {
248 		b
249 	}
250 }
251 
252 `;
253 
254 	auto d = lexAndParse(s);
255 	auto foo = d.astSelect!Field("foo.a.b");
256 	assert(foo !is null);
257 	assert(foo.name.name.value == "b");
258 }
259 
260 unittest {
261 	string s = `
262 fragment Foo on Bar {
263 	a @skip(if: true)
264 }
265 
266 query foo {
267 	...Foo
268 }
269 
270 `;
271 
272 	auto d = lexAndParse(s);
273 	auto a = d.astSelect!Field("foo.a");
274 	assert(a !is null);
275 	assert(a.dirs !is null);
276 	assert(a.dirs.dir.name.value == "skip");
277 }
278 
279 import std.range : take;
280 import graphql.helper : lexAndParse;
281 
282 struct RandomPaths {
283 	import std.random : choice, Random;
284 	import std.algorithm.sorting : sort;
285 	import std.algorithm.iteration : uniq, splitter, map, joiner;
286 	import std.conv : to;
287 	import std.range : take, iota;
288 	import std.array : array;
289 	import std.ascii : isWhite;
290 	import std..string : strip, split;
291 	string[] elems;
292 	string toIgnore;
293 	size_t len;
294 
295 	string front;
296 	Random rnd;
297 
298 	static RandomPaths opCall(string elems, string toIgnore, uint seed) {
299 		RandomPaths ret;
300 
301 		ret.elems = elems.splitter!isWhite()
302 			.map!(e => e.strip)
303 			.array
304 			.sort
305 			.uniq
306 			.array;
307 
308 		ret.toIgnore = toIgnore;
309 		ret.rnd = Random(seed);
310 		ret.len = toIgnore.split('.').length;
311 		return ret;
312 	}
313 
314 	private void build() {
315 		do {
316 			this.front = iota(this.len)
317 				.map!(i => choice(this.elems, this.rnd))
318 				.joiner(".")
319 				.to!string();
320 		} while(this.front == this.toIgnore);
321 	}
322 
323 	void popFront() {
324 		this.build();
325 	}
326 
327 	enum bool empty = false;
328 }
329 
330 unittest {
331 	import std.array : array;
332 	import std.algorithm.sorting : sort;
333 	import std.algorithm.iteration : uniq;
334 	import std.format : format;
335 
336 	string s = ` query foo { a { b } c { b } foo ( args : 10 ) { ... on Foo {
337 			bar } } } mutation a { foo @ ship ( if : true) { b } } `;
338 	string toIgnore = "foo.a.b";
339 
340 	string[] r = take(RandomPaths(s, toIgnore, 1337), 10).array.sort.release;
341 	string[] su = r.dup.sort.uniq.array;
342 	assert(r == su, format("\n%s\n%s", r, su));
343 }
344 
345 unittest {
346 	string s = `
347 query foo {
348 	a {
349 		b
350 	}
351 	c {
352 		b
353 	}
354 	foo ( args : 10 ) {
355 		... on Foo {
356 			bar
357 		}
358 	}
359 }
360 
361 mutation a {
362 	foo @ ship ( if : true) {
363 		b
364 	}
365 }
366 `;
367 
368 	auto d = lexAndParse(s);
369 	string toIgnore = "foo.a.b";
370 	const(Field) foo = astSelect!Field(d, toIgnore);
371 	foreach(string p; take(RandomPaths(s, toIgnore, 1337), 5000)) {
372 		auto bar = astSelect!Field(d, p);
373 		assert(bar is null || bar !is foo);
374 	}
375 }