Skip to content

Commit be9e561

Browse files
committed
improvement: add possible_type_extensions validator
1 parent b371b1a commit be9e561

File tree

4 files changed

+273
-3
lines changed

4 files changed

+273
-3
lines changed
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import "package:gql/ast.dart";
2+
import "package:gql/document.dart" show ValidationError;
3+
import "package:gql/src/validation/validating_visitor.dart";
4+
5+
class PossibleTypeExtensionError extends ValidationError {
6+
PossibleTypeExtensionError.nodeNotDefined(
7+
TypeExtensionNode node,
8+
) : super(
9+
message:
10+
'Cannot extend type "${node.name.value}" because it is not defined.',
11+
node: node,
12+
);
13+
14+
PossibleTypeExtensionError.incorrectType(
15+
TypeExtensionNode node, {
16+
required String kind,
17+
}) : super(
18+
message: 'Cannot extend non-${kind} type "${node.name.value}".',
19+
node: node,
20+
);
21+
}
22+
23+
/// Possible type extension
24+
///
25+
/// A type extension is only valid if the type is defined and has the same kind.
26+
class PossibleTypeExtensions extends ValidatingVisitor {
27+
final _definedTypes = <String, TypeDefinitionNode>{};
28+
29+
List<ValidationError>? _recordType(TypeDefinitionNode node) {
30+
_definedTypes[node.name.value] = node;
31+
return null;
32+
}
33+
34+
List<ValidationError>? _validateType<T extends TypeDefinitionNode>(
35+
TypeExtensionNode node, {
36+
required String kind,
37+
}) {
38+
final typeDefinition = _definedTypes[node.name.value];
39+
if (typeDefinition == null) {
40+
return [PossibleTypeExtensionError.nodeNotDefined(node)];
41+
}
42+
if (typeDefinition is! T) {
43+
return [PossibleTypeExtensionError.incorrectType(node, kind: kind)];
44+
}
45+
return null;
46+
}
47+
48+
// Enum
49+
@override
50+
List<ValidationError>? visitEnumTypeDefinitionNode(
51+
EnumTypeDefinitionNode node) =>
52+
_recordType(node);
53+
54+
@override
55+
List<ValidationError>? visitEnumTypeExtensionNode(
56+
EnumTypeExtensionNode node) =>
57+
_validateType<EnumTypeDefinitionNode>(node, kind: "enum");
58+
59+
// Input Object
60+
@override
61+
List<ValidationError>? visitInputObjectTypeDefinitionNode(
62+
InputObjectTypeDefinitionNode node) =>
63+
_recordType(node);
64+
65+
@override
66+
List<ValidationError>? visitInputObjectTypeExtensionNode(
67+
InputObjectTypeExtensionNode node) =>
68+
_validateType<InputObjectTypeDefinitionNode>(node, kind: "input");
69+
70+
// Union
71+
@override
72+
List<ValidationError>? visitUnionTypeDefinitionNode(
73+
UnionTypeDefinitionNode node) =>
74+
_recordType(node);
75+
76+
@override
77+
List<ValidationError>? visitUnionTypeExtensionNode(
78+
UnionTypeExtensionNode node) =>
79+
_validateType<UnionTypeDefinitionNode>(node, kind: "union");
80+
81+
// Interface
82+
@override
83+
List<ValidationError>? visitInterfaceTypeDefinitionNode(
84+
InterfaceTypeDefinitionNode node) =>
85+
_recordType(node);
86+
87+
@override
88+
List<ValidationError>? visitInterfaceTypeExtensionNode(
89+
InterfaceTypeExtensionNode node) =>
90+
_validateType<InterfaceTypeDefinitionNode>(node, kind: "interface");
91+
92+
// Object
93+
@override
94+
List<ValidationError>? visitObjectTypeDefinitionNode(
95+
ObjectTypeDefinitionNode node) =>
96+
_recordType(node);
97+
98+
@override
99+
List<ValidationError>? visitObjectTypeExtensionNode(
100+
ObjectTypeExtensionNode node) =>
101+
_validateType<ObjectTypeDefinitionNode>(node, kind: "object");
102+
103+
// Scalar
104+
@override
105+
List<ValidationError>? visitScalarTypeDefinitionNode(
106+
ScalarTypeDefinitionNode node) =>
107+
_recordType(node);
108+
109+
@override
110+
List<ValidationError>? visitScalarTypeExtensionNode(
111+
ScalarTypeExtensionNode node) =>
112+
_validateType<ScalarTypeDefinitionNode>(node, kind: "scalar");
113+
}

gql/lib/src/validation/validator.dart

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import "package:gql/ast.dart" as ast;
22
import "package:gql/src/validation/rules/lone_schema_definition.dart";
33
import "package:gql/src/validation/rules/missing_fragment_definitions.dart";
4+
import "package:gql/src/validation/rules/possible_type_extensions.dart";
45
import "package:gql/src/validation/rules/unique_argument_names.dart";
56
import "package:gql/src/validation/rules/unique_directive_names.dart";
67
import "package:gql/src/validation/rules/unique_enum_value_names.dart";
@@ -86,6 +87,9 @@ abstract class ValidationError {
8687
this.message,
8788
this.node,
8889
});
90+
91+
@override
92+
String toString() => message ?? super.toString();
8993
}
9094

9195
/// Available validation rules
@@ -98,7 +102,8 @@ enum ValidationRule {
98102
uniqueTypeNames,
99103
uniqueInputFieldNames,
100104
uniqueArgumentNames,
101-
missingFragmentDefinition
105+
missingFragmentDefinition,
106+
possibleTypeExtensions,
102107
}
103108

104109
ValidatingVisitor? _mapRule(ValidationRule rule) {
@@ -121,8 +126,8 @@ ValidatingVisitor? _mapRule(ValidationRule rule) {
121126
return UniqueArgumentNames();
122127
case ValidationRule.missingFragmentDefinition:
123128
return const MissingFragmentDefinition();
124-
default:
125-
return null;
129+
case ValidationRule.possibleTypeExtensions:
130+
return PossibleTypeExtensions();
126131
}
127132
}
128133

gql/test/validation/common.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ Matcher errorOfType<T extends ValidationError>() => predicate(
66
(ValidationError error) => error is T,
77
);
88

9+
Matcher errorOfTypeWithMessage<T extends ValidationError>(String message) =>
10+
predicate(
11+
(ValidationError error) => error is T && message == error.message,
12+
);
13+
914
Iterable<ValidationError> Function(String) createValidator(
1015
Set<ValidationRule> rules,
1116
) =>
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import "package:gql/src/validation/validator.dart";
2+
import "package:test/test.dart";
3+
4+
import "./common.dart";
5+
6+
final validate = createValidator({
7+
ValidationRule.possibleTypeExtensions,
8+
});
9+
10+
void main() {
11+
group("Possible type extensions", () {
12+
test("one extension per type", () {
13+
expect(
14+
validate(
15+
"""
16+
scalar FooScalar
17+
type FooObject
18+
interface FooInterface
19+
union FooUnion
20+
enum FooEnum
21+
input FooInputObject
22+
23+
extend scalar FooScalar @dummy
24+
extend type FooObject @dummy
25+
extend interface FooInterface @dummy
26+
extend union FooUnion @dummy
27+
extend enum FooEnum @dummy
28+
extend input FooInputObject @dummy
29+
""",
30+
).map((it) => it.toString()),
31+
equals([]),
32+
);
33+
});
34+
35+
test("multiple extension per type", () {
36+
expect(
37+
validate(
38+
"""
39+
scalar FooScalar
40+
type FooObject
41+
interface FooInterface
42+
union FooUnion
43+
enum FooEnum
44+
input FooInputObject
45+
46+
extend scalar FooScalar @dummy
47+
extend type FooObject @dummy
48+
extend interface FooInterface @dummy
49+
extend union FooUnion @dummy
50+
extend enum FooEnum @dummy
51+
extend input FooInputObject @dummy
52+
53+
extend scalar FooScalar @dummy
54+
extend type FooObject @dummy
55+
extend interface FooInterface @dummy
56+
extend union FooUnion @dummy
57+
extend enum FooEnum @dummy
58+
extend input FooInputObject @dummy
59+
""",
60+
).map((it) => it.toString()),
61+
equals([]),
62+
);
63+
});
64+
65+
test("ignores non type definitions", () {
66+
expect(
67+
validate(
68+
"""
69+
query Foo { __typename }
70+
fragment Foo on Query { __typename }
71+
directive @Foo on SCHEMA
72+
73+
extend scalar Foo @dummy
74+
extend type Foo @dummy
75+
extend interface Foo @dummy
76+
extend union Foo @dummy
77+
extend enum Foo @dummy
78+
extend input Foo @dummy
79+
""",
80+
).map((it) => it.toString()),
81+
equals([
82+
'Cannot extend type "Foo" because it is not defined.',
83+
'Cannot extend type "Foo" because it is not defined.',
84+
'Cannot extend type "Foo" because it is not defined.',
85+
'Cannot extend type "Foo" because it is not defined.',
86+
'Cannot extend type "Foo" because it is not defined.',
87+
'Cannot extend type "Foo" because it is not defined.',
88+
]),
89+
);
90+
});
91+
92+
test("undefined type", () {
93+
expect(
94+
validate(
95+
"""
96+
type Known
97+
98+
extend scalar Unknown @dummy
99+
extend type Unknown @dummy
100+
extend interface Unknown @dummy
101+
extend union Unknown @dummy
102+
extend enum Unknown @dummy
103+
extend input Unknown @dummy
104+
""",
105+
).map((it) => it.toString()),
106+
equals([
107+
'Cannot extend type "Unknown" because it is not defined.',
108+
'Cannot extend type "Unknown" because it is not defined.',
109+
'Cannot extend type "Unknown" because it is not defined.',
110+
'Cannot extend type "Unknown" because it is not defined.',
111+
'Cannot extend type "Unknown" because it is not defined.',
112+
'Cannot extend type "Unknown" because it is not defined.',
113+
]),
114+
);
115+
});
116+
117+
test("incorrect types", () {
118+
expect(
119+
validate(
120+
"""
121+
scalar FooScalar
122+
type FooObject
123+
interface FooInterface
124+
union FooUnion
125+
enum FooEnum
126+
input FooInputObject
127+
128+
extend type FooScalar @dummy
129+
extend interface FooObject @dummy
130+
extend union FooInterface @dummy
131+
extend enum FooUnion @dummy
132+
extend input FooEnum @dummy
133+
extend scalar FooInputObject @dummy
134+
""",
135+
).map((it) => it.toString()),
136+
equals([
137+
'Cannot extend non-object type "FooScalar".',
138+
'Cannot extend non-interface type "FooObject".',
139+
'Cannot extend non-union type "FooInterface".',
140+
'Cannot extend non-enum type "FooUnion".',
141+
'Cannot extend non-input type "FooEnum".',
142+
'Cannot extend non-scalar type "FooInputObject".',
143+
]),
144+
);
145+
});
146+
});
147+
}

0 commit comments

Comments
 (0)