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 }