Skip to content

Commit bd09770

Browse files
feature: Implements oneOf directive
1 parent 8171c0e commit bd09770

File tree

9 files changed

+430
-19
lines changed

9 files changed

+430
-19
lines changed

Sources/GraphQL/Type/Definition.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1217,12 +1217,14 @@ public final class GraphQLInputObjectType {
12171217
public let name: String
12181218
public let description: String?
12191219
public let fields: InputObjectFieldDefinitionMap
1220+
public let isOneOf: Bool
12201221
public let kind: TypeKind = .inputObject
12211222

12221223
public init(
12231224
name: String,
12241225
description: String? = nil,
1225-
fields: InputObjectFieldMap = [:]
1226+
fields: InputObjectFieldMap = [:],
1227+
isOneOf: Bool = false
12261228
) throws {
12271229
try assertValid(name: name)
12281230
self.name = name
@@ -1231,6 +1233,7 @@ public final class GraphQLInputObjectType {
12311233
name: name,
12321234
fields: fields
12331235
)
1236+
self.isOneOf = isOneOf
12341237
}
12351238

12361239
func replaceTypeReferences(typeMap: TypeMap) throws {
@@ -1245,6 +1248,7 @@ extension GraphQLInputObjectType: Encodable {
12451248
case name
12461249
case description
12471250
case fields
1251+
case isOneOf
12481252
case kind
12491253
}
12501254
}
@@ -1258,6 +1262,8 @@ extension GraphQLInputObjectType: KeySubscriptable {
12581262
return description
12591263
case CodingKeys.fields.rawValue:
12601264
return fields
1265+
case CodingKeys.isOneOf.rawValue:
1266+
return isOneOf
12611267
case CodingKeys.kind.rawValue:
12621268
return kind
12631269
default:

Sources/GraphQL/Type/Directives.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,22 @@ public let GraphQLDeprecatedDirective = try! GraphQLDirective(
123123
]
124124
)
125125

126+
/**
127+
* Used to indicate an Input Object is a OneOf Input Object.
128+
*/
129+
public let GraphQLOneOfDirective = try! GraphQLDirective(
130+
name: "oneOf",
131+
description: "Indicates exactly one field must be supplied and this field must not be `null`.",
132+
locations: [.inputObject],
133+
args: [:]
134+
)
135+
126136
/**
127137
* The full list of specified directives.
128138
*/
129139
let specifiedDirectives: [GraphQLDirective] = [
130140
GraphQLIncludeDirective,
131141
GraphQLSkipDirective,
132142
GraphQLDeprecatedDirective,
143+
GraphQLOneOfDirective,
133144
]

Sources/GraphQL/Type/Introspection.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,6 +310,15 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
310310
}
311311
),
312312
"ofType": GraphQLField(type: GraphQLTypeReference("__Type")),
313+
"isOneOf": GraphQLField(
314+
type: GraphQLBoolean,
315+
resolve: { type, _, _, _ in
316+
if let type = type as? GraphQLInputObjectType {
317+
return type.isOneOf
318+
}
319+
return false
320+
}
321+
),
313322
]
314323
)
315324

Sources/GraphQL/Utilities/IsValidValue.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,22 @@ func validate(value: Map, forType type: GraphQLInputType) throws -> [String] {
6363
}
6464
}
6565

66+
// Ensure only one field in oneOf input is defined
67+
if objectType.isOneOf {
68+
let keys = dictionary.filter { $1 != .undefined }.keys
69+
if keys.count != 1 {
70+
errors.append(
71+
"Exactly one key must be specified for OneOf type \"\(objectType.name)\"."
72+
)
73+
}
74+
75+
let key = keys[0]
76+
let value = dictionary[key]
77+
if value == .null {
78+
errors.append("Field \"\(key)\" must be non-null.")
79+
}
80+
}
81+
6682
// Ensure every defined field is valid.
6783
for (fieldName, field) in fields {
6884
let newErrors = try validate(value: value[fieldName], forType: field.type).map {

Sources/GraphQL/Utilities/ValueFromAST.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,18 @@ func valueFromAST(
9999
}
100100
}
101101
}
102+
103+
if objectType.isOneOf {
104+
let keys = object.filter { $1 != .undefined }.keys
105+
if keys.count != 1 {
106+
return .undefined // Invalid: not exactly one key, intentionally return no value.
107+
}
108+
109+
if object[keys[0]] == .null {
110+
return .undefined // Invalid: value not non-null, intentionally return no value.
111+
}
112+
}
113+
102114
return .dictionary(object)
103115
}
104116

Sources/GraphQL/Validation/Rules/ValuesOfCorrectTypeRule.swift

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,9 @@ func ValuesOfCorrectTypeRule(context: ValidationContext) -> Visitor {
3737
return .break // Don't traverse further.
3838
}
3939
// Ensure every required field exists.
40-
let fieldNodeMap = Dictionary(grouping: object.fields) { field in
41-
field.name.value
40+
var fieldNodeMap = [String: ObjectField]()
41+
for field in object.fields {
42+
fieldNodeMap[field.name.value] = field
4243
}
4344
for (fieldName, fieldDef) in type.fields {
4445
if fieldNodeMap[fieldName] == nil, isRequiredInputField(fieldDef) {
@@ -52,7 +53,15 @@ func ValuesOfCorrectTypeRule(context: ValidationContext) -> Visitor {
5253
}
5354
}
5455

55-
// TODO: Add oneOf support
56+
if type.isOneOf {
57+
validateOneOfInputObject(
58+
context: context,
59+
node: object,
60+
type: type,
61+
fieldNodeMap: fieldNodeMap,
62+
variableDefinitions: variableDefinitions
63+
)
64+
}
5665
return .continue
5766
}
5867
if let field = node as? ObjectField {
@@ -172,3 +181,55 @@ func isValidValueNode(_ context: ValidationContext, _ node: Value) {
172181
}
173182
}
174183
}
184+
185+
func validateOneOfInputObject(
186+
context: ValidationContext,
187+
node: ObjectValue,
188+
type: GraphQLInputObjectType,
189+
fieldNodeMap: [String: ObjectField],
190+
variableDefinitions: [String: VariableDefinition]
191+
) {
192+
let keys = Array(fieldNodeMap.keys)
193+
let isNotExactlyOneField = keys.count != 1
194+
195+
if isNotExactlyOneField {
196+
context.report(
197+
error: GraphQLError(
198+
message: "OneOf Input Object \"\(type.name)\" must specify exactly one key.",
199+
nodes: [node]
200+
)
201+
)
202+
return
203+
}
204+
205+
let value = fieldNodeMap[keys[0]]?.value
206+
let isNullLiteral = value == nil || value?.kind == .nullValue
207+
208+
if isNullLiteral {
209+
context.report(
210+
error: GraphQLError(
211+
message: "Field \"\(type.name).\(keys[0])\" must be non-null.",
212+
nodes: [node]
213+
)
214+
)
215+
return
216+
}
217+
218+
if let value = value, value.kind == .variable {
219+
let variable = value as! Variable // Force unwrap is safe because of variable definition
220+
let variableName = variable.name.value
221+
222+
if
223+
let definition = variableDefinitions[variableName],
224+
definition.type.kind != .nonNullType
225+
{
226+
context.report(
227+
error: GraphQLError(
228+
message: "Variable \"\(variableName)\" must be non-nullable to be used for OneOf Input Object \"\(type.name)\".",
229+
nodes: [node]
230+
)
231+
)
232+
return
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)