Skip to content

Commit 87649db

Browse files
Merge pull request #144 from NeedleInAJayStack/feature/new-directives
Adds `oneOf` and `specifiedBy` directives
2 parents 8171c0e + cb45688 commit 87649db

File tree

9 files changed

+454
-20
lines changed

9 files changed

+454
-20
lines changed

Sources/GraphQL/Type/Definition.swift

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,7 @@ extension GraphQLNonNull: GraphQLWrapperType {}
169169
public final class GraphQLScalarType {
170170
public let name: String
171171
public let description: String?
172+
public let specifiedByURL: String?
172173
public let kind: TypeKind = .scalar
173174

174175
let serialize: (Any) throws -> Map
@@ -178,13 +179,15 @@ public final class GraphQLScalarType {
178179
public init(
179180
name: String,
180181
description: String? = nil,
182+
specifiedByURL: String? = nil,
181183
serialize: @escaping (Any) throws -> Map,
182184
parseValue: ((Map) throws -> Map)? = nil,
183185
parseLiteral: ((Value) throws -> Map)? = nil
184186
) throws {
185187
try assertValid(name: name)
186188
self.name = name
187189
self.description = description
190+
self.specifiedByURL = specifiedByURL
188191
self.serialize = serialize
189192
self.parseValue = parseValue ?? defaultParseValue
190193
self.parseLiteral = parseLiteral ?? defaultParseLiteral
@@ -218,6 +221,7 @@ extension GraphQLScalarType: Encodable {
218221
private enum CodingKeys: String, CodingKey {
219222
case name
220223
case description
224+
case specifiedByURL
221225
case kind
222226
}
223227
}
@@ -229,6 +233,8 @@ extension GraphQLScalarType: KeySubscriptable {
229233
return name
230234
case CodingKeys.description.rawValue:
231235
return description
236+
case CodingKeys.specifiedByURL.rawValue:
237+
return specifiedByURL
232238
case CodingKeys.kind.rawValue:
233239
return kind
234240
default:
@@ -1217,12 +1223,14 @@ public final class GraphQLInputObjectType {
12171223
public let name: String
12181224
public let description: String?
12191225
public let fields: InputObjectFieldDefinitionMap
1226+
public let isOneOf: Bool
12201227
public let kind: TypeKind = .inputObject
12211228

12221229
public init(
12231230
name: String,
12241231
description: String? = nil,
1225-
fields: InputObjectFieldMap = [:]
1232+
fields: InputObjectFieldMap = [:],
1233+
isOneOf: Bool = false
12261234
) throws {
12271235
try assertValid(name: name)
12281236
self.name = name
@@ -1231,6 +1239,7 @@ public final class GraphQLInputObjectType {
12311239
name: name,
12321240
fields: fields
12331241
)
1242+
self.isOneOf = isOneOf
12341243
}
12351244

12361245
func replaceTypeReferences(typeMap: TypeMap) throws {
@@ -1245,6 +1254,7 @@ extension GraphQLInputObjectType: Encodable {
12451254
case name
12461255
case description
12471256
case fields
1257+
case isOneOf
12481258
case kind
12491259
}
12501260
}
@@ -1258,6 +1268,8 @@ extension GraphQLInputObjectType: KeySubscriptable {
12581268
return description
12591269
case CodingKeys.fields.rawValue:
12601270
return fields
1271+
case CodingKeys.isOneOf.rawValue:
1272+
return isOneOf
12611273
case CodingKeys.kind.rawValue:
12621274
return kind
12631275
default:

Sources/GraphQL/Type/Directives.swift

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

126+
/**
127+
* Used to provide a URL for specifying the behavior of custom scalar definitions.
128+
*/
129+
public let GraphQLSpecifiedByDirective = try! GraphQLDirective(
130+
name: "specifiedBy",
131+
description: "Exposes a URL that specifies the behavior of this scalar.",
132+
locations: [.scalar],
133+
args: [
134+
"url": GraphQLArgument(
135+
type: GraphQLNonNull(GraphQLString),
136+
description: "The URL that specifies the behavior of this scalar."
137+
),
138+
]
139+
)
140+
141+
/**
142+
* Used to indicate an Input Object is a OneOf Input Object.
143+
*/
144+
public let GraphQLOneOfDirective = try! GraphQLDirective(
145+
name: "oneOf",
146+
description: "Indicates exactly one field must be supplied and this field must not be `null`.",
147+
locations: [.inputObject],
148+
args: [:]
149+
)
150+
126151
/**
127152
* The full list of specified directives.
128153
*/
129154
let specifiedDirectives: [GraphQLDirective] = [
130155
GraphQLIncludeDirective,
131156
GraphQLSkipDirective,
132157
GraphQLDeprecatedDirective,
158+
GraphQLSpecifiedByDirective,
159+
GraphQLOneOfDirective,
133160
]

Sources/GraphQL/Type/Introspection.swift

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -185,7 +185,7 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
185185
"many kinds of types in GraphQL as represented by the `__TypeKind` enum." +
186186
"\n\nDepending on the kind of a type, certain fields describe " +
187187
"information about that type. Scalar types provide no information " +
188-
"beyond a name and description, while Enum types provide their values. " +
188+
"beyond a name and description and optional `specifiedByURL`, while Enum types provide their values. " +
189189
"Object and Interface types provide the fields they describe. Abstract " +
190190
"types, Union and Interface, provide the Object types possible " +
191191
"at runtime. List and NonNull types compose other types.",
@@ -217,6 +217,7 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
217217
),
218218
"name": GraphQLField(type: GraphQLString),
219219
"description": GraphQLField(type: GraphQLString),
220+
"specifiedByURL": GraphQLField(type: GraphQLString),
220221
"fields": GraphQLField(
221222
type: GraphQLList(GraphQLNonNull(__Field)),
222223
args: [
@@ -310,6 +311,15 @@ let __Type: GraphQLObjectType = try! GraphQLObjectType(
310311
}
311312
),
312313
"ofType": GraphQLField(type: GraphQLTypeReference("__Type")),
314+
"isOneOf": GraphQLField(
315+
type: GraphQLBoolean,
316+
resolve: { type, _, _, _ in
317+
if let type = type as? GraphQLInputObjectType {
318+
return type.isOneOf
319+
}
320+
return false
321+
}
322+
),
313323
]
314324
)
315325

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)