Skip to content

Commit 31795b0

Browse files
feature: ExecutableDefinitionsRule
1 parent 031d6c3 commit 31795b0

File tree

4 files changed

+164
-2
lines changed

4 files changed

+164
-2
lines changed

Sources/GraphQL/Language/AST.swift

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1769,7 +1769,10 @@ extension OperationTypeDefinition: Equatable {
17691769
}
17701770
}
17711771

1772-
public protocol TypeDefinition: TypeSystemDefinition {}
1772+
public protocol TypeDefinition: TypeSystemDefinition {
1773+
var name: Name { get }
1774+
}
1775+
17731776
extension ScalarTypeDefinition: TypeDefinition {}
17741777
extension ObjectTypeDefinition: TypeDefinition {}
17751778
extension InterfaceTypeDefinition: TypeDefinition {}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
2+
/**
3+
* Executable definitions
4+
*
5+
* A GraphQL document is only valid for execution if all definitions are either
6+
* operation or fragment definitions.
7+
*
8+
* See https://spec.graphql.org/draft/#sec-Executable-Definitions
9+
*/
10+
func ExecutableDefinitionsRule(context: ValidationContext) -> Visitor {
11+
let definitions = context.ast.definitions
12+
let existingTypesMap = context.schema.typeMap
13+
14+
var typeNames = Set<String>()
15+
for typeName in existingTypesMap.keys {
16+
typeNames.insert(typeName)
17+
}
18+
for definition in definitions {
19+
if
20+
let type = definition as? TypeDefinition,
21+
let nameResult = type.get(key: "name"),
22+
case let .node(nameNode) = nameResult,
23+
let name = nameNode as? Name
24+
{
25+
typeNames.insert(name.value)
26+
}
27+
}
28+
29+
return Visitor(
30+
enter: { node, _, _, _, _ in
31+
if let node = node as? Document {
32+
for definition in node.definitions {
33+
if !isExecutable(definition) {
34+
var defName = "schema"
35+
if let definition = definition as? TypeDefinition {
36+
defName = "\"\(definition.name.value)\""
37+
} else if let definition = definition as? TypeExtensionDefinition {
38+
defName = "\"\(definition.definition.name.value)\""
39+
}
40+
context.report(
41+
error: GraphQLError(
42+
message: "The \(defName) definition is not executable.",
43+
nodes: [definition]
44+
)
45+
)
46+
}
47+
}
48+
}
49+
return .continue
50+
}
51+
)
52+
}
53+
54+
func isExecutable(_ definition: Definition) -> Bool {
55+
definition.kind == .operationDefinition || definition
56+
.kind == .fragmentDefinition
57+
}

Sources/GraphQL/Validation/SpecifiedRules.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
* This set includes all validation rules defined by the GraphQL spec.
33
*/
44
public let specifiedRules: [(ValidationContext) -> Visitor] = [
5-
// ExecutableDefinitionsRule,
5+
ExecutableDefinitionsRule,
66
UniqueOperationNamesRule,
77
LoneAnonymousOperationRule,
88
// SingleFieldSubscriptionsRule,
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
@testable import GraphQL
2+
import XCTest
3+
4+
class ExecutableDefinitionsRuleTests: ValidationTestCase {
5+
override func setUp() {
6+
rule = ExecutableDefinitionsRule
7+
}
8+
9+
func testWithOnlyOperation() throws {
10+
try assertValid(
11+
"""
12+
query Foo {
13+
dog {
14+
name
15+
}
16+
}
17+
"""
18+
)
19+
}
20+
21+
func testWithOperationAndFragment() throws {
22+
try assertValid(
23+
"""
24+
query Foo {
25+
dog {
26+
name
27+
...Frag
28+
}
29+
}
30+
31+
fragment Frag on Dog {
32+
name
33+
}
34+
"""
35+
)
36+
}
37+
38+
func testWithTypeDefinition() throws {
39+
let errors = try assertInvalid(
40+
errorCount: 2,
41+
query: """
42+
query Foo {
43+
dog {
44+
name
45+
}
46+
}
47+
48+
type Cow {
49+
name: String
50+
}
51+
52+
extend type Dog {
53+
color: String
54+
}
55+
"""
56+
)
57+
58+
try assertValidationError(
59+
error: errors[0],
60+
locations: [(line: 7, column: 1)],
61+
message: #"The "Cow" definition is not executable."#
62+
)
63+
try assertValidationError(
64+
error: errors[1],
65+
locations: [(line: 11, column: 1)],
66+
message: #"The "Dog" definition is not executable."#
67+
)
68+
}
69+
70+
func testWithSchemaDefinition() throws {
71+
let errors = try assertInvalid(
72+
errorCount: 3,
73+
query: """
74+
schema {
75+
query: Query
76+
}
77+
78+
type Query {
79+
test: String
80+
}
81+
82+
extend schema @directive
83+
"""
84+
)
85+
86+
try assertValidationError(
87+
error: errors[0],
88+
locations: [(line: 1, column: 1)],
89+
message: #"The schema definition is not executable."#
90+
)
91+
try assertValidationError(
92+
error: errors[1],
93+
locations: [(line: 5, column: 1)],
94+
message: #"The "Query" definition is not executable."#
95+
)
96+
try assertValidationError(
97+
error: errors[2],
98+
locations: [(line: 9, column: 1)],
99+
message: #"The schema definition is not executable."#
100+
)
101+
}
102+
}

0 commit comments

Comments
 (0)