Skip to content

Commit e383f79

Browse files
Merge pull request #90 from NeedleInAJayStack/fix/variableNullLiterals
Variable and argument null vs undefined
2 parents 15073b4 + c6fbc1f commit e383f79

File tree

7 files changed

+1086
-154
lines changed

7 files changed

+1086
-154
lines changed

Sources/GraphQL/Execution/Execute.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,7 @@ func shouldIncludeNode(exeContext: ExecutionContext, directives: [Directive] = [
594594
let skip = try getArgumentValues(
595595
argDefs: GraphQLSkipDirective.args,
596596
argASTs: skipAST.arguments,
597-
variableValues: exeContext.variableValues
597+
variables: exeContext.variableValues
598598
)
599599

600600
if skip["if"] == .bool(true) {
@@ -606,7 +606,7 @@ func shouldIncludeNode(exeContext: ExecutionContext, directives: [Directive] = [
606606
let include = try getArgumentValues(
607607
argDefs: GraphQLIncludeDirective.args,
608608
argASTs: includeAST.arguments,
609-
variableValues: exeContext.variableValues
609+
variables: exeContext.variableValues
610610
)
611611

612612
if include["if"] == .bool(false) {
@@ -685,7 +685,7 @@ public func resolveField(
685685
let args = try getArgumentValues(
686686
argDefs: fieldDef.args,
687687
argASTs: fieldAST.arguments,
688-
variableValues: exeContext.variableValues
688+
variables: exeContext.variableValues
689689
)
690690

691691
// The resolve func's optional third argument is a context value that
+93-92
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,76 @@
11
import Foundation
2+
import OrderedCollections
23

34
/**
45
* Prepares an object map of variableValues of the correct type based on the
56
* provided variable definitions and arbitrary input. If the input cannot be
67
* parsed to match the variable definitions, a GraphQLError will be thrown.
78
*/
89
func getVariableValues(schema: GraphQLSchema, definitionASTs: [VariableDefinition], inputs: [String: Map]) throws -> [String: Map] {
9-
return try definitionASTs.reduce([:]) { values, defAST in
10-
var valuesCopy = values
10+
11+
var vars = [String: Map]()
12+
for defAST in definitionASTs {
1113
let varName = defAST.variable.name.value
12-
13-
valuesCopy[varName] = try getVariableValue(
14+
15+
let input: Map
16+
if let nonNilInput = inputs[varName] {
17+
input = nonNilInput
18+
} else {
19+
// If variable is not in inputs it is undefined
20+
input = .undefined
21+
}
22+
vars[varName] = try getVariableValue(
1423
schema: schema,
1524
definitionAST: defAST,
16-
input: inputs[varName] ?? .null
25+
input: input
1726
)
18-
19-
return valuesCopy
2027
}
28+
return vars
2129
}
2230

2331

2432
/**
2533
* Prepares an object map of argument values given a list of argument
2634
* definitions and list of argument AST nodes.
2735
*/
28-
func getArgumentValues(argDefs: [GraphQLArgumentDefinition], argASTs: [Argument]?, variableValues: [String: Map] = [:]) throws -> Map {
36+
func getArgumentValues(argDefs: [GraphQLArgumentDefinition], argASTs: [Argument]?, variables: [String: Map] = [:]) throws -> Map {
2937
guard let argASTs = argASTs else {
3038
return [:]
3139
}
3240

3341
let argASTMap = argASTs.keyMap({ $0.name.value })
34-
35-
return try argDefs.reduce([:]) { result, argDef in
36-
var result = result
37-
let name = argDef.name
38-
let argAST = argASTMap[name]
42+
43+
var args = OrderedDictionary<String, Map>()
44+
for argDef in argDefs {
45+
let argName = argDef.name
46+
let argValue: Map
3947

40-
if let argAST = argAST {
41-
let valueAST = argAST.value
42-
43-
let value = try valueFromAST(
44-
valueAST: valueAST,
48+
if let argAST = argASTMap[argName] {
49+
argValue = try valueFromAST(
50+
valueAST: argAST.value,
4551
type: argDef.type,
46-
variables: variableValues
52+
variables: variables
4753
)
48-
49-
result[name] = value
5054
} else {
51-
result[name] = .null
55+
// If AST doesn't contain field, it is undefined
56+
if let defaultValue = argDef.defaultValue {
57+
argValue = defaultValue
58+
} else {
59+
argValue = .undefined
60+
}
5261
}
53-
54-
return result
62+
63+
let errors = try validate(value: argValue, forType: argDef.type)
64+
guard errors.isEmpty else {
65+
let message = "\n" + errors.joined(separator: "\n")
66+
throw GraphQLError(
67+
message:
68+
"Argument \"\(argName)\" got invalid value \(argValue).\(message)" // TODO: "\(JSON.stringify(input)).\(message)",
69+
)
70+
}
71+
args[argName] = argValue
5572
}
73+
return .dictionary(args)
5674
}
5775

5876

@@ -64,112 +82,95 @@ func getVariableValue(schema: GraphQLSchema, definitionAST: VariableDefinition,
6482
let type = typeFromAST(schema: schema, inputTypeAST: definitionAST.type)
6583
let variable = definitionAST.variable
6684

67-
if type == nil || !isInputType(type: type) {
85+
guard let inputType = type as? GraphQLInputType else {
6886
throw GraphQLError(
6987
message:
7088
"Variable \"$\(variable.name.value)\" expected value of type " +
7189
"\"\(definitionAST.type)\" which cannot be used as an input type.",
7290
nodes: [definitionAST]
7391
)
7492
}
75-
76-
let inputType = type as! GraphQLInputType
77-
let errors = try isValidValue(value: input, type: inputType)
78-
79-
if errors.isEmpty {
80-
if input == .null {
81-
if let defaultValue = definitionAST.defaultValue {
82-
return try valueFromAST(valueAST: defaultValue, type: inputType)
83-
}
84-
else if !(inputType is GraphQLNonNull) {
85-
return .null
86-
}
87-
}
88-
89-
return try coerceValue(type: inputType, value: input)!
93+
94+
var toCoerce = input
95+
if input == .undefined, let defaultValue = definitionAST.defaultValue {
96+
toCoerce = try valueFromAST(valueAST: defaultValue, type: inputType)
9097
}
91-
92-
guard input != .null else {
98+
99+
let errors = try validate(value: toCoerce, forType: inputType)
100+
guard errors.isEmpty else {
101+
let message = !errors.isEmpty ? "\n" + errors.joined(separator: "\n") : ""
93102
throw GraphQLError(
94103
message:
95-
"Variable \"$\(variable.name.value)\" of required type " +
96-
"\"\(definitionAST.type)\" was not provided.",
104+
"Variable \"$\(variable.name.value)\" got invalid value \"\(toCoerce)\".\(message)", // TODO: "\(JSON.stringify(input)).\(message)",
97105
nodes: [definitionAST]
98106
)
99107
}
100-
101-
let message = !errors.isEmpty ? "\n" + errors.joined(separator: "\n") : ""
102-
103-
throw GraphQLError(
104-
message:
105-
"Variable \"$\(variable.name.value)\" got invalid value " +
106-
"\(input).\(message)", // TODO: "\(JSON.stringify(input)).\(message)",
107-
nodes: [definitionAST]
108-
)
108+
109+
return try coerceValue(value: toCoerce, type: inputType)
109110
}
110111

111112
/**
112113
* Given a type and any value, return a runtime value coerced to match the type.
113114
*/
114-
func coerceValue(type: GraphQLInputType, value: Map) throws -> Map? {
115+
func coerceValue(value: Map, type: GraphQLInputType) throws -> Map {
115116
if let nonNull = type as? GraphQLNonNull {
116117
// Note: we're not checking that the result of coerceValue is non-null.
117-
// We only call this function after calling isValidValue.
118-
return try coerceValue(type: nonNull.ofType as! GraphQLInputType, value: value)!
118+
// We only call this function after calling validate.
119+
guard let nonNullType = nonNull.ofType as? GraphQLInputType else {
120+
throw GraphQLError(message: "NonNull must wrap an input type")
121+
}
122+
return try coerceValue(value: value, type: nonNullType)
119123
}
120-
121-
guard value != .null else {
122-
return nil
124+
125+
guard value != .undefined && value != .null else {
126+
return value
123127
}
124128

125129
if let list = type as? GraphQLList {
126-
let itemType = list.ofType
130+
guard let itemType = list.ofType as? GraphQLInputType else {
131+
throw GraphQLError(message: "Input list must wrap an input type")
132+
}
127133

128134
if case .array(let value) = value {
129-
var coercedValues: [Map] = []
130-
131-
for item in value {
132-
coercedValues.append(try coerceValue(type: itemType as! GraphQLInputType, value: item)!)
135+
let coercedValues = try value.map { item in
136+
try coerceValue(value: item, type: itemType)
133137
}
134-
135138
return .array(coercedValues)
136139
}
137-
138-
return .array([try coerceValue(type: itemType as! GraphQLInputType, value: value)!])
140+
141+
// Convert solitary value into single-value array
142+
return .array([try coerceValue(value: value, type: itemType)])
139143
}
140144

141-
if let type = type as? GraphQLInputObjectType {
145+
if let objectType = type as? GraphQLInputObjectType {
142146
guard case .dictionary(let value) = value else {
143-
return nil
147+
throw GraphQLError(message: "Must be dictionary to extract to an input type")
144148
}
145149

146-
let fields = type.fields
147-
148-
return try .dictionary(fields.keys.reduce([:]) { obj, fieldName in
149-
var objCopy = obj
150-
let field = fields[fieldName]
151-
152-
var fieldValue = try coerceValue(type: field!.type, value: value[fieldName] ?? .null)
153-
154-
if fieldValue == .null {
155-
fieldValue = field.flatMap({ $0.defaultValue })
150+
let fields = objectType.fields
151+
152+
var object = OrderedDictionary<String, Map>()
153+
for (fieldName, field) in fields {
154+
if let fieldValueMap = value[fieldName], fieldValueMap != .undefined {
155+
object[fieldName] = try coerceValue(
156+
value: fieldValueMap,
157+
type: field.type
158+
)
156159
} else {
157-
objCopy[fieldName] = fieldValue
160+
// If AST doesn't contain field, it is undefined
161+
if let defaultValue = field.defaultValue {
162+
object[fieldName] = defaultValue
163+
} else {
164+
object[fieldName] = .undefined
165+
}
158166
}
159-
160-
return objCopy
161-
})
167+
}
168+
return .dictionary(object)
162169
}
163170

164-
guard let type = type as? GraphQLLeafType else {
165-
throw GraphQLError(message: "Must be input type")
171+
if let leafType = type as? GraphQLLeafType {
172+
return try leafType.parseValue(value: value)
166173
}
167174

168-
let parsed = try type.parseValue(value: value)
169-
170-
guard parsed != .null else {
171-
return nil
172-
}
173-
174-
return parsed
175+
throw GraphQLError(message: "Provided type is not an input type")
175176
}

Sources/GraphQL/Map/Map.swift

+2
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,8 @@ extension Map : Equatable {}
709709

710710
public func == (lhs: Map, rhs: Map) -> Bool {
711711
switch (lhs, rhs) {
712+
case (.undefined, .undefined):
713+
return true
712714
case (.null, .null):
713715
return true
714716
case let (.bool(l), .bool(r)) where l == r:

Sources/GraphQL/Subscription/Subscribe.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ func executeSubscription(
195195

196196
// Build a map of arguments from the field.arguments AST, using the
197197
// variables scope to fulfill any variable references.
198-
let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variableValues: context.variableValues)
198+
let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variables: context.variableValues)
199199

200200
// The resolve function's optional third argument is a context value that
201201
// is provided to every resolve function within an execution. It is commonly

0 commit comments

Comments
 (0)