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