1 module graphql.validation.schemabased;
2 
3 import std.algorithm.iteration : map;
4 import std.algorithm.searching : canFind;
5 import std.array : array, back, empty, front, popBack;
6 import std.conv : to;
7 import std.meta : staticMap, NoDuplicates;
8 import std.exception : enforce, assertThrown, assertNotThrown;
9 import std.format : format;
10 import std.stdio;
11 import std.string : strip;
12 
13 import vibe.data.json;
14 
15 import fixedsizearray;
16 
17 import graphql.ast;
18 import graphql.builder;
19 import graphql.constants;
20 import graphql.visitor;
21 import graphql.schema.types;
22 import graphql.schema.helper;
23 import graphql.validation.exception;
24 import graphql.helper : lexAndParse;
25 
26 @safe:
27 
28 string astTypeToString(const(Type) input) pure {
29 	final switch(input.ruleSelection) {
30 		case TypeEnum.TN:
31 			return format!"%s!"(input.tname.value);
32 		case TypeEnum.LN:
33 			return format!"[%s]!"(astTypeToString(input.list.type));
34 		case TypeEnum.T:
35 			return format!"%s"(input.tname.value);
36 		case TypeEnum.L:
37 			return format!"[%s]"(astTypeToString(input.list.type));
38 	}
39 }
40 
41 enum IsSubscription {
42 	no,
43 	yes
44 }
45 
46 struct TypePlusName {
47 	Json type;
48 	string name;
49 
50 	string toString() const {
51 		return format("%s %s", this.name, this.type);
52 	}
53 }
54 
55 class SchemaValidator(Schema) : Visitor {
56 	import std.experimental.typecons : Final;
57 	import graphql.schema.typeconversions;
58 	import graphql.traits;
59 	import graphql.helper : stringTypeStrip;
60 
61 	alias enter = Visitor.enter;
62 	alias exit = Visitor.exit;
63 	alias accept = Visitor.accept;
64 
65 	const(Document) doc;
66 	GQLDSchema!(Schema) schema;
67 
68 	// Single root field
69 	IsSubscription isSubscription;
70 	int ssCnt;
71 	int selCnt;
72 
73 	// Field Selections on Objects
74 	TypePlusName[] schemaStack;
75 
76 	// Variables of operation
77 	Type[string] variables;
78 
79 	void addToTypeStack(string name) {
80 		//writefln("\n\nFoo '%s' %s", name, this.schemaStack.map!(a => a.name));
81 
82 		enforce!FieldDoesNotExist(
83 				Constants.fields in this.schemaStack.back.type,
84 				format("Type '%s' does not have fields",
85 					this.schemaStack.back.name)
86 			);
87 
88 		enforce!FieldDoesNotExist(
89 				this.schemaStack.back.type.type == Json.Type.object,
90 				format("Field '%s' of type '%s' is not a Json.Type.object",
91 					name, this.schemaStack.back.name)
92 			);
93 
94 		immutable toFindIn = [Constants.__typename, Constants.__schema,
95 					Constants.__type];
96 		Json field = canFind(toFindIn, name)
97 			? getIntrospectionField(name)
98 			: this.schemaStack.back.type.getField(name);
99 		enforce!FieldDoesNotExist(
100 				field.type == Json.Type.object,
101 				format("Type '%s' does not have fields named '%s'",
102 					this.schemaStack.back.name, name)
103 			);
104 
105 
106 		string followType = field[Constants.typenameOrig].get!string();
107 		string old = followType;
108 		followType = followType.stringTypeStrip();
109 		this.addTypeToStackImpl(name, followType, old);
110 	}
111 
112 	void addTypeToStackImpl(string name, string followType, string old) {
113 		l: switch(followType) {
114 			alias AllTypes = collectTypesPlusIntrospection!(Schema);
115 			alias Stripped = staticMap!(stripArrayAndNullable, AllTypes);
116 			alias NoDups = NoDuplicates!(Stripped);
117 			static foreach(type; NoDups) {{
118 				case typeToTypeName!(type): {
119 					this.schemaStack ~= TypePlusName(
120 							removeNonNullAndList(typeToJson!(type,Schema)()),
121 							name
122 						);
123 					//writeln(this.schemaStack.back.type.toPrettyString());
124 					break l;
125 				}
126 			}}
127 			default:
128 				throw new UnknownTypeName(
129 						format("No type with name '%s' '%s' is known",
130 							followType, old), __FILE__, __LINE__);
131 		}
132 	}
133 
134 	this(const(Document) doc, GQLDSchema!(Schema) schema) {
135 		this.doc = doc;
136 		this.schema = schema;
137 		this.schemaStack ~= TypePlusName(
138 				removeNonNullAndList(typeToJson!(Schema,Schema)()),
139 				Schema.stringof
140 			);
141 	}
142 
143 	override void enter(const(OperationType) ot) {
144 		this.isSubscription = ot.ruleSelection == OperationTypeEnum.Sub
145 			? IsSubscription.yes : IsSubscription.no;
146 	}
147 
148 	override void enter(const(SelectionSet) ss) {
149 		//writeln(this.schemaStack);
150 		++this.ssCnt;
151 	}
152 
153 	override void exit(const(SelectionSet) ss) {
154 		--this.ssCnt;
155 	}
156 
157 	override void enter(const(Selection) sel) {
158 		++this.selCnt;
159 		const bool notSingleRootField = this.isSubscription == IsSubscription.yes
160 				&& this.ssCnt == 1
161 				&& this.selCnt > 1;
162 
163 		enforce!SingleRootField(!notSingleRootField);
164 	}
165 
166 	override void enter(const(FragmentDefinition) fragDef) {
167 		string typeName = fragDef.tc.value;
168 		//writefln("%s %s", typeName, fragDef.name.value);
169 		l: switch(typeName) {
170 			alias AllTypes = collectTypesPlusIntrospection!(Schema);
171 			alias Stripped = staticMap!(stripArrayAndNullable, AllTypes);
172 			alias NoDups = NoDuplicates!(Stripped);
173 			static foreach(type; NoDups) {{
174 				case typeToTypeName!(type): {
175 					this.schemaStack ~= TypePlusName(
176 							removeNonNullAndList(typeToJson!(type,Schema)()),
177 							typeName
178 						);
179 					//writeln(this.schemaStack.back.type.toPrettyString());
180 					break l;
181 				}
182 			}}
183 			default:
184 				throw new UnknownTypeName(
185 						format("No type with name '%s' is known",
186 							typeName), __FILE__, __LINE__);
187 		}
188 	}
189 
190 	override void enter(const(FragmentSpread) fragSpread) {
191 		enum uo = ["OBJECT", "UNION", "INTERFACE"];
192 		enforce!FragmentNotOnCompositeType(
193 				"kind" in this.schemaStack.back.type
194 				&& canFind(uo, this.schemaStack.back.type["kind"].get!string()),
195 				format("'%s' is not an %(%s, %)",
196 					this.schemaStack.back.type.toPrettyString(), uo)
197 			);
198 		const(FragmentDefinition) frag = findFragment(this.doc,
199 				fragSpread.name.value
200 			);
201 		frag.visit(this);
202 	}
203 
204 	override void enter(const(OperationDefinition) op) {
205 		string name = op.ruleSelection == OperationDefinitionEnum.SelSet
206 				|| op.ot.ruleSelection == OperationTypeEnum.Query
207 			? "queryType"
208 			: op.ot.ruleSelection == OperationTypeEnum.Mutation
209 				?	"mutationType"
210 				: op.ot.ruleSelection == OperationTypeEnum.Sub
211 					? "subscriptionType"
212 					: "";
213 		enforce(!name.empty);
214 		this.addToTypeStack(name);
215 	}
216 
217 	override void accept(const(Field) f) {
218 		super.accept(f);
219 		enforce!LeafIsNotAScalar(f.ss !is null ||
220 				(this.schemaStack.back.type["kind"].get!string() == "SCALAR"
221 				|| this.schemaStack.back.type["kind"].get!string() == "ENUM"),
222 				format("Leaf field '%s' is not a SCALAR but '%s'",
223 					this.schemaStack.back.name,
224 					this.schemaStack.back.type.toPrettyString())
225 				);
226 	}
227 
228 	override void enter(const(FieldName) fn) {
229 		this.addToTypeStack(fn.name.value);
230 	}
231 
232 	override void enter(const(InlineFragment) inF) {
233 		this.addTypeToStackImpl("InlineFragment", inF.tc.value, "");
234 	}
235 
236 	override void exit(const(InlineFragment) inF) {
237 		this.schemaStack.popBack();
238 	}
239 
240 	override void exit(const(Selection) op) {
241 		this.schemaStack.popBack();
242 	}
243 
244 	override void exit(const(OperationDefinition) op) {
245 		this.schemaStack.popBack();
246 	}
247 
248 	override void enter(const(VariableDefinition) vd) {
249 		const vdName = vd.var.name.value;
250 		() @trusted {
251 			this.variables[vdName] = cast()vd.type;
252 		}();
253 	}
254 
255 	override void enter(const(Argument) arg) {
256 		import std.algorithm.searching : find;
257 		const argName = arg.name.value;
258 		const parent = this.schemaStack[$ - 2];
259 		const curName = this.schemaStack.back.name;
260 		auto fields = parent.type[Constants.fields];
261 		if(fields.type != Json.Type.Array) {
262 			return;
263 		}
264 		auto curNameFieldRange = fields.byValue
265 			.find!(f => f[Constants.name].to!string() == curName);
266 		if(curNameFieldRange.empty) {
267 			return;
268 		}
269 
270 		auto curNameField = curNameFieldRange.front;
271 
272 		Json curArgs = curNameField[Constants.args];
273 		auto argElem = curArgs.byValue.find!(a => a[Constants.name] == argName);
274 
275 		enforce!ArgumentDoesNotExist(!argElem.empty, format!(
276 				"Argument with name '%s' does not exist for field '%s' of type "
277 				~ " '%s'")(argName, curName, parent.type[Constants.name]));
278 
279 		if(arg.vv.ruleSelection == ValueOrVariableEnum.Var) {
280 			const varName = arg.vv.var.name.value;
281 			auto varType = varName in this.variables;
282 			enforce(varName !is null);
283 
284 			string typeStr = astTypeToString(*varType);
285 			enforce!VariableInputTypeMismatch(
286 					argElem.front[Constants.typenameOrig] == typeStr,
287 					format!"Variable type '%s' does not match argument type '%s'"
288 					(argElem.front[Constants.typenameOrig], typeStr));
289 		}
290 	}
291 }
292 
293 import graphql.testschema;
294 
295 private void test(T)(string str) {
296 	GQLDSchema!(Schema) testSchema = new GQLDSchema!(Schema)();
297 	auto doc = lexAndParse(str);
298 	auto fv = new SchemaValidator!Schema(doc, testSchema);
299 
300 	static if(is(T == void)) {
301 		assertNotThrown(fv.accept(doc));
302 	} else {
303 		assertThrown!T(fv.accept(doc));
304 	}
305 }
306 
307 unittest {
308 	string str = `
309 subscription sub {
310 	starships {
311 		id
312 		name
313 	}
314 }`;
315 	test!void(str);
316 }
317 
318 unittest {
319 	string str = `
320 subscription sub {
321 	starships {
322 		id
323 		name
324 	}
325 	starships {
326 		size
327 	}
328 }`;
329 
330 	test!SingleRootField(str);
331 }
332 
333 unittest {
334 	string str = `
335 subscription sub {
336 	...multipleSubscriptions
337 }
338 
339 fragment multipleSubscriptions on Subscription {
340 	starships {
341 		id
342 		name
343 	}
344 	starships {
345 	size
346 	}
347 }`;
348 
349 	test!SingleRootField(str);
350 }
351 
352 unittest {
353 	string str = `
354 subscription sub {
355 	starships {
356 		id
357 		name
358 	}
359 	__typename
360 }`;
361 
362 	test!SingleRootField(str);
363 }
364 
365 unittest {
366 	string str = `
367 {
368 	starships {
369 		id
370 	}
371 }
372 `;
373 
374 	test!void(str);
375 }
376 
377 unittest {
378 	string str = `
379 {
380 	starships {
381 		fieldDoesNotExist
382 	}
383 }
384 `;
385 
386 	test!FieldDoesNotExist(str);
387 }
388 
389 unittest {
390 	string str = `
391 {
392 	starships {
393 		id {
394 			name
395 		}
396 	}
397 }
398 `;
399 
400 	test!FieldDoesNotExist(str);
401 }
402 
403 unittest {
404 	string str = `
405 query q {
406 	search {
407 		shipId
408 	}
409 }`;
410 
411 	test!FieldDoesNotExist(str);
412 }
413 
414 unittest {
415 	string str = `
416 query q {
417 	search {
418 		...ShipFrag
419 	}
420 }
421 
422 fragment ShipFrag on Starship {
423 	designation
424 }
425 `;
426 
427 	test!void(str);
428 }
429 
430 unittest {
431 	string str = `
432 query q {
433 	search {
434 		...ShipFrag
435 		...CharFrag
436 	}
437 }
438 
439 fragment ShipFrag on Starship {
440 	designation
441 }
442 
443 fragment CharFrag on Character {
444 	foobar
445 }
446 `;
447 
448 	test!FieldDoesNotExist(str);
449 }
450 
451 unittest {
452 	string str = `
453 mutation q {
454 	addCrewman {
455 		...CharFrag
456 	}
457 }
458 
459 fragment CharFrag on Character {
460 	name
461 }
462 `;
463 
464 	test!void(str);
465 }
466 
467 unittest {
468 	string str = `
469 mutation q {
470 	addCrewman {
471 		...CharFrag
472 	}
473 }
474 
475 fragment CharFrag on Character {
476 	foobar
477 }
478 `;
479 
480 	test!FieldDoesNotExist(str);
481 }
482 
483 unittest {
484 	string str = `
485 subscription q {
486 	starships {
487 		id
488 		designation
489 	}
490 }
491 `;
492 
493 	test!void(str);
494 }
495 
496 unittest {
497 	string str = `
498 subscription q {
499 	starships {
500 		id
501 		doesNotExist
502 	}
503 }
504 `;
505 
506 	test!FieldDoesNotExist(str);
507 }
508 
509 unittest {
510 	string str = `
511 query q {
512 	search {
513 		...ShipFrag
514 		...CharFrag
515 	}
516 }
517 
518 fragment ShipFrag on Starship {
519 	designation
520 }
521 
522 fragment CharFrag on Character {
523 	name
524 }
525 `;
526 
527 	test!void(str);
528 }
529 
530 unittest {
531 	string str = `
532 {
533 	starships {
534 		__typename
535 	}
536 }
537 `;
538 
539 	test!void(str);
540 }
541 
542 unittest {
543 	string str = `
544 {
545 	__schema {
546 		types {
547 			name
548 		}
549 	}
550 }
551 `;
552 
553 	test!void(str);
554 }
555 
556 unittest {
557 	string str = `
558 {
559 	__schema {
560 		types {
561 			enumValues {
562 				name
563 			}
564 		}
565 	}
566 }
567 `;
568 
569 	test!void(str);
570 }
571 
572 unittest {
573 	string str = `
574 {
575 	__schema {
576 		types {
577 			enumValues {
578 				doesNotExist
579 			}
580 		}
581 	}
582 }
583 `;
584 
585 	test!FieldDoesNotExist(str);
586 }
587 
588 unittest {
589 	string str = `
590 query q {
591 	search {
592 		...CharFrag
593 	}
594 }
595 
596 fragment CharFrag on NonExistingType {
597 	name
598 }
599 `;
600 
601 	test!UnknownTypeName(str);
602 }
603 
604 unittest {
605 	string str = `
606 query q {
607 	search {
608 		...CharFrag
609 	}
610 }
611 
612 fragment CharFrag on Character {
613 	name
614 }
615 `;
616 
617 	test!void(str);
618 }
619 
620 unittest {
621 	string str = `
622 query q {
623 	starships {
624 		id {
625 			...CharFrag
626 		}
627 	}
628 }
629 
630 fragment CharFrag on Character {
631 	name {
632 		foo
633 	}
634 }
635 `;
636 
637 	test!FragmentNotOnCompositeType(str);
638 }
639 
640 unittest {
641 	string str = `
642 query q {
643 	starships {
644 		crew
645 	}
646 }
647 `;
648 
649 	test!LeafIsNotAScalar(str);
650 }
651 
652 unittest {
653 	string str = `
654 query q {
655 	starships {
656 		crew {
657 			name
658 		}
659 	}
660 }
661 `;
662 
663 	test!void(str);
664 }
665 
666 unittest {
667 	string str = `
668 {
669 	starships {
670 		crew {
671 			id
672 			ship
673 		}
674 	}
675 }
676 `;
677 
678 	test!LeafIsNotAScalar(str);
679 }
680 
681 unittest {
682 	string str = `
683 {
684 	starships {
685 		crew {
686 			id
687 			ships
688 		}
689 	}
690 }`;
691 
692 	test!LeafIsNotAScalar(str);
693 }
694 
695 unittest {
696 	string str = `
697 {
698 	starships
699 }`;
700 
701 	test!LeafIsNotAScalar(str);
702 }
703 
704 unittest {
705 	string str = `
706 query q($size: String) {
707 	starships(overSize: $size) {
708 		id
709 	}
710 }`;
711 
712 	test!VariableInputTypeMismatch(str);
713 }
714 
715 unittest {
716 	string str = `
717 query q($size: Float!) {
718 	starships(overSize: $size) {
719 		id
720 	}
721 }`;
722 
723 	test!void(str);
724 }
725 
726 unittest {
727 	string str = `
728 query q($ships: [Int!]!) {
729 	shipsselection(ids: $ships) {
730 		id
731 	}
732 }`;
733 
734 	test!void(str);
735 }
736 
737 unittest {
738 	string str = `
739 {
740 	starships {
741 		crew {
742 			... on Humanoid {
743 				dateOfBirth
744 			}
745 		}
746 	}
747 }`;
748 
749 	test!void(str);
750 }
751 
752 unittest {
753 	string str = `
754 {
755 	starships {
756 		crew {
757 			... on Humanoid {
758 				doesNotExist
759 			}
760 		}
761 	}
762 }`;
763 
764 	test!FieldDoesNotExist(str);
765 }