Skip to content

Commit 6dde6dc

Browse files
feat: Adds ProvidedRequiredArgumentsOnDirectivesRule
1 parent a372889 commit 6dde6dc

File tree

3 files changed

+183
-1
lines changed

3 files changed

+183
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
2+
func ProvidedRequiredArgumentsOnDirectivesRule(
3+
context: SDLorNormalValidationContext
4+
) -> Visitor {
5+
var requiredArgsMap = [String: [String: String]]()
6+
7+
let schema = context.getSchema()
8+
let definedDirectives = schema?.directives ?? specifiedDirectives
9+
for directive in definedDirectives {
10+
var requiredArgs = [String: String]()
11+
for arg in directive.args.filter({ isRequiredArgument($0) }) {
12+
requiredArgs[arg.name] = arg.type.debugDescription
13+
}
14+
requiredArgsMap[directive.name] = requiredArgs
15+
}
16+
17+
let astDefinitions = context.ast.definitions
18+
for def in astDefinitions {
19+
if let def = def as? DirectiveDefinition {
20+
let argNodes = def.arguments
21+
var requiredArgs = [String: String]()
22+
for arg in argNodes.filter({ isRequiredArgumentNode($0) }) {
23+
requiredArgs[arg.name.value] = print(ast: arg.type)
24+
}
25+
requiredArgsMap[def.name.value] = requiredArgs
26+
}
27+
}
28+
29+
return Visitor(
30+
// Validate on leave to allow for deeper errors to appear first.
31+
leave: { node, _, _, _, _ in
32+
if let directiveNode = node as? Directive {
33+
let directiveName = directiveNode.name.value
34+
if let requiredArgs = requiredArgsMap[directiveName] {
35+
let argNodes = directiveNode.arguments
36+
let argNodeMap = Set(argNodes.map(\.name.value))
37+
for (argName, argType) in requiredArgs {
38+
if !argNodeMap.contains(argName) {
39+
context.report(
40+
error: GraphQLError(
41+
message: "Argument \"@\(directiveName)(\(argName):)\" of type \"\(argType)\" is required, but it was not provided.",
42+
nodes: [directiveNode]
43+
)
44+
)
45+
}
46+
}
47+
}
48+
}
49+
return .continue
50+
}
51+
)
52+
}
53+
54+
func isRequiredArgumentNode(
55+
arg: InputValueDefinition
56+
) -> Bool {
57+
return arg.type.kind == .nonNullType && arg.defaultValue == nil
58+
}
59+
60+
func isRequiredArgumentNode(
61+
arg: VariableDefinition
62+
) -> Bool {
63+
return arg.type.kind == .nonNullType && arg.defaultValue == nil
64+
}

Sources/GraphQL/Validation/SpecifiedRules.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,5 +51,5 @@ public let specifiedSDLRules: [SDLValidationRule] = [
5151
KnownArgumentNamesOnDirectivesRule,
5252
UniqueArgumentNamesRule,
5353
UniqueInputFieldNamesRule,
54-
// ProvidedRequiredArgumentsOnDirectivesRule,
54+
ProvidedRequiredArgumentsOnDirectivesRule,
5555
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
@testable import GraphQL
2+
import XCTest
3+
4+
class ProvidedRequiredArgumentsOnDirectivesRuleTests: SDLValidationTestCase {
5+
override func setUp() {
6+
rule = ProvidedRequiredArgumentsOnDirectivesRule
7+
}
8+
9+
func testMissingOptionalArgsOnDirectiveDefinedInsideSDL() throws {
10+
try assertValidationErrors(
11+
"""
12+
type Query {
13+
foo: String @test
14+
}
15+
16+
directive @test(arg1: String, arg2: String! = "") on FIELD_DEFINITION
17+
""",
18+
[]
19+
)
20+
}
21+
22+
func testMissingArgOnDirectiveDefinedInsideSDL() throws {
23+
try assertValidationErrors(
24+
"""
25+
type Query {
26+
foo: String @test
27+
}
28+
29+
directive @test(arg: String!) on FIELD_DEFINITION
30+
""",
31+
[
32+
GraphQLError(
33+
message: #"Argument "@test(arg:)" of type "String!" is required, but it was not provided."#,
34+
locations: [.init(line: 2, column: 15)]
35+
),
36+
]
37+
)
38+
}
39+
40+
func testMissingArgOnStandardDirective() throws {
41+
try assertValidationErrors(
42+
"""
43+
type Query {
44+
foo: String @include
45+
}
46+
""",
47+
[
48+
GraphQLError(
49+
message: #"Argument "@include(if:)" of type "Boolean!" is required, but it was not provided."#,
50+
locations: [.init(line: 2, column: 15)]
51+
),
52+
]
53+
)
54+
}
55+
56+
func testMissingArgOnOveriddenStandardDirective() throws {
57+
try assertValidationErrors(
58+
"""
59+
type Query {
60+
foo: String @deprecated
61+
}
62+
directive @deprecated(reason: String!) on FIELD
63+
""",
64+
[
65+
GraphQLError(
66+
message: #"Argument "@deprecated(reason:)" of type "String!" is required, but it was not provided."#,
67+
locations: [.init(line: 2, column: 15)]
68+
),
69+
]
70+
)
71+
}
72+
73+
func testMissingArgOnDirectiveDefinedInSchemaExtension() throws {
74+
let schema = try buildSchema(source: """
75+
type Query {
76+
foo: String
77+
}
78+
""")
79+
let sdl = """
80+
directive @test(arg: String!) on OBJECT
81+
82+
extend type Query @test
83+
"""
84+
try assertValidationErrors(
85+
sdl,
86+
schema: schema,
87+
[
88+
GraphQLError(
89+
message: #"Argument "@test(arg:)" of type "String!" is required, but it was not provided."#,
90+
locations: [.init(line: 3, column: 20)]
91+
),
92+
]
93+
)
94+
}
95+
96+
func testMissingArgOnDirectiveUsedInSchemaExtension() throws {
97+
let schema = try buildSchema(source: """
98+
directive @test(arg: String!) on OBJECT
99+
100+
type Query {
101+
foo: String
102+
}
103+
""")
104+
let sdl = """
105+
extend type Query @test
106+
"""
107+
try assertValidationErrors(
108+
sdl,
109+
schema: schema,
110+
[
111+
GraphQLError(
112+
message: #"Argument "@test(arg:)" of type "String!" is required, but it was not provided."#,
113+
locations: [.init(line: 1, column: 19)]
114+
),
115+
]
116+
)
117+
}
118+
}

0 commit comments

Comments
 (0)