diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 6ab5c1aa..486eb994 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -594,7 +594,7 @@ func shouldIncludeNode(exeContext: ExecutionContext, directives: [Directive] = [ let skip = try getArgumentValues( argDefs: GraphQLSkipDirective.args, argASTs: skipAST.arguments, - variableValues: exeContext.variableValues + variables: exeContext.variableValues ) if skip["if"] == .bool(true) { @@ -606,7 +606,7 @@ func shouldIncludeNode(exeContext: ExecutionContext, directives: [Directive] = [ let include = try getArgumentValues( argDefs: GraphQLIncludeDirective.args, argASTs: includeAST.arguments, - variableValues: exeContext.variableValues + variables: exeContext.variableValues ) if include["if"] == .bool(false) { @@ -685,7 +685,7 @@ public func resolveField( let args = try getArgumentValues( argDefs: fieldDef.args, argASTs: fieldAST.arguments, - variableValues: exeContext.variableValues + variables: exeContext.variableValues ) // The resolve func's optional third argument is a context value that diff --git a/Sources/GraphQL/Execution/Values.swift b/Sources/GraphQL/Execution/Values.swift index 29eb15e2..0336f4e4 100644 --- a/Sources/GraphQL/Execution/Values.swift +++ b/Sources/GraphQL/Execution/Values.swift @@ -1,4 +1,5 @@ import Foundation +import OrderedCollections /** * Prepares an object map of variableValues of the correct type based on the @@ -6,18 +7,25 @@ import Foundation * parsed to match the variable definitions, a GraphQLError will be thrown. */ func getVariableValues(schema: GraphQLSchema, definitionASTs: [VariableDefinition], inputs: [String: Map]) throws -> [String: Map] { - return try definitionASTs.reduce([:]) { values, defAST in - var valuesCopy = values + + var vars = [String: Map]() + for defAST in definitionASTs { let varName = defAST.variable.name.value - - valuesCopy[varName] = try getVariableValue( + + let input: Map + if let nonNilInput = inputs[varName] { + input = nonNilInput + } else { + // If variable is not in inputs it is undefined + input = .undefined + } + vars[varName] = try getVariableValue( schema: schema, definitionAST: defAST, - input: inputs[varName] ?? .null + input: input ) - - return valuesCopy } + return vars } @@ -25,34 +33,44 @@ func getVariableValues(schema: GraphQLSchema, definitionASTs: [VariableDefinitio * Prepares an object map of argument values given a list of argument * definitions and list of argument AST nodes. */ -func getArgumentValues(argDefs: [GraphQLArgumentDefinition], argASTs: [Argument]?, variableValues: [String: Map] = [:]) throws -> Map { +func getArgumentValues(argDefs: [GraphQLArgumentDefinition], argASTs: [Argument]?, variables: [String: Map] = [:]) throws -> Map { guard let argASTs = argASTs else { return [:] } let argASTMap = argASTs.keyMap({ $0.name.value }) - - return try argDefs.reduce([:]) { result, argDef in - var result = result - let name = argDef.name - let argAST = argASTMap[name] + + var args = OrderedDictionary() + for argDef in argDefs { + let argName = argDef.name + let argValue: Map - if let argAST = argAST { - let valueAST = argAST.value - - let value = try valueFromAST( - valueAST: valueAST, + if let argAST = argASTMap[argName] { + argValue = try valueFromAST( + valueAST: argAST.value, type: argDef.type, - variables: variableValues + variables: variables ) - - result[name] = value } else { - result[name] = .null + // If AST doesn't contain field, it is undefined + if let defaultValue = argDef.defaultValue { + argValue = defaultValue + } else { + argValue = .undefined + } } - - return result + + let errors = try validate(value: argValue, forType: argDef.type) + guard errors.isEmpty else { + let message = "\n" + errors.joined(separator: "\n") + throw GraphQLError( + message: + "Argument \"\(argName)\" got invalid value \(argValue).\(message)" // TODO: "\(JSON.stringify(input)).\(message)", + ) + } + args[argName] = argValue } + return .dictionary(args) } @@ -64,7 +82,7 @@ func getVariableValue(schema: GraphQLSchema, definitionAST: VariableDefinition, let type = typeFromAST(schema: schema, inputTypeAST: definitionAST.type) let variable = definitionAST.variable - if type == nil || !isInputType(type: type) { + guard let inputType = type as? GraphQLInputType else { throw GraphQLError( message: "Variable \"$\(variable.name.value)\" expected value of type " + @@ -72,104 +90,87 @@ func getVariableValue(schema: GraphQLSchema, definitionAST: VariableDefinition, nodes: [definitionAST] ) } - - let inputType = type as! GraphQLInputType - let errors = try isValidValue(value: input, type: inputType) - - if errors.isEmpty { - if input == .null { - if let defaultValue = definitionAST.defaultValue { - return try valueFromAST(valueAST: defaultValue, type: inputType) - } - else if !(inputType is GraphQLNonNull) { - return .null - } - } - - return try coerceValue(type: inputType, value: input)! + + var toCoerce = input + if input == .undefined, let defaultValue = definitionAST.defaultValue { + toCoerce = try valueFromAST(valueAST: defaultValue, type: inputType) } - - guard input != .null else { + + let errors = try validate(value: toCoerce, forType: inputType) + guard errors.isEmpty else { + let message = !errors.isEmpty ? "\n" + errors.joined(separator: "\n") : "" throw GraphQLError( message: - "Variable \"$\(variable.name.value)\" of required type " + - "\"\(definitionAST.type)\" was not provided.", + "Variable \"$\(variable.name.value)\" got invalid value \"\(toCoerce)\".\(message)", // TODO: "\(JSON.stringify(input)).\(message)", nodes: [definitionAST] ) } - - let message = !errors.isEmpty ? "\n" + errors.joined(separator: "\n") : "" - - throw GraphQLError( - message: - "Variable \"$\(variable.name.value)\" got invalid value " + - "\(input).\(message)", // TODO: "\(JSON.stringify(input)).\(message)", - nodes: [definitionAST] - ) + + return try coerceValue(value: toCoerce, type: inputType) } /** * Given a type and any value, return a runtime value coerced to match the type. */ -func coerceValue(type: GraphQLInputType, value: Map) throws -> Map? { +func coerceValue(value: Map, type: GraphQLInputType) throws -> Map { if let nonNull = type as? GraphQLNonNull { // Note: we're not checking that the result of coerceValue is non-null. - // We only call this function after calling isValidValue. - return try coerceValue(type: nonNull.ofType as! GraphQLInputType, value: value)! + // We only call this function after calling validate. + guard let nonNullType = nonNull.ofType as? GraphQLInputType else { + throw GraphQLError(message: "NonNull must wrap an input type") + } + return try coerceValue(value: value, type: nonNullType) } - - guard value != .null else { - return nil + + guard value != .undefined && value != .null else { + return value } if let list = type as? GraphQLList { - let itemType = list.ofType + guard let itemType = list.ofType as? GraphQLInputType else { + throw GraphQLError(message: "Input list must wrap an input type") + } if case .array(let value) = value { - var coercedValues: [Map] = [] - - for item in value { - coercedValues.append(try coerceValue(type: itemType as! GraphQLInputType, value: item)!) + let coercedValues = try value.map { item in + try coerceValue(value: item, type: itemType) } - return .array(coercedValues) } - - return .array([try coerceValue(type: itemType as! GraphQLInputType, value: value)!]) + + // Convert solitary value into single-value array + return .array([try coerceValue(value: value, type: itemType)]) } - if let type = type as? GraphQLInputObjectType { + if let objectType = type as? GraphQLInputObjectType { guard case .dictionary(let value) = value else { - return nil + throw GraphQLError(message: "Must be dictionary to extract to an input type") } - let fields = type.fields - - return try .dictionary(fields.keys.reduce([:]) { obj, fieldName in - var objCopy = obj - let field = fields[fieldName] - - var fieldValue = try coerceValue(type: field!.type, value: value[fieldName] ?? .null) - - if fieldValue == .null { - fieldValue = field.flatMap({ $0.defaultValue }) + let fields = objectType.fields + + var object = OrderedDictionary() + for (fieldName, field) in fields { + if let fieldValueMap = value[fieldName], fieldValueMap != .undefined { + object[fieldName] = try coerceValue( + value: fieldValueMap, + type: field.type + ) } else { - objCopy[fieldName] = fieldValue + // If AST doesn't contain field, it is undefined + if let defaultValue = field.defaultValue { + object[fieldName] = defaultValue + } else { + object[fieldName] = .undefined + } } - - return objCopy - }) + } + return .dictionary(object) } - guard let type = type as? GraphQLLeafType else { - throw GraphQLError(message: "Must be input type") + if let leafType = type as? GraphQLLeafType { + return try leafType.parseValue(value: value) } - let parsed = try type.parseValue(value: value) - - guard parsed != .null else { - return nil - } - - return parsed + throw GraphQLError(message: "Provided type is not an input type") } diff --git a/Sources/GraphQL/Map/Map.swift b/Sources/GraphQL/Map/Map.swift index f185889b..dfde29d0 100644 --- a/Sources/GraphQL/Map/Map.swift +++ b/Sources/GraphQL/Map/Map.swift @@ -709,6 +709,8 @@ extension Map : Equatable {} public func == (lhs: Map, rhs: Map) -> Bool { switch (lhs, rhs) { + case (.undefined, .undefined): + return true case (.null, .null): return true case let (.bool(l), .bool(r)) where l == r: diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index a7a79394..d10076c0 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -195,7 +195,7 @@ func executeSubscription( // Build a map of arguments from the field.arguments AST, using the // variables scope to fulfill any variable references. - let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variableValues: context.variableValues) + let args = try getArgumentValues(argDefs: fieldDef.args, argASTs: fieldNode.arguments, variables: context.variableValues) // The resolve function's optional third argument is a context value that // is provided to every resolve function within an execution. It is commonly diff --git a/Sources/GraphQL/Utilities/IsValidValue.swift b/Sources/GraphQL/Utilities/IsValidValue.swift index 6edec555..a7a30395 100644 --- a/Sources/GraphQL/Utilities/IsValidValue.swift +++ b/Sources/GraphQL/Utilities/IsValidValue.swift @@ -3,33 +3,39 @@ * accepted for that type. This is primarily useful for validating the * runtime values of query variables. */ -func isValidValue(value: Map, type: GraphQLInputType) throws -> [String] { +func validate(value: Map, forType type: GraphQLInputType) throws -> [String] { // A value must be provided if the type is non-null. - if let type = type as? GraphQLNonNull { - if value == .null { - if let namedType = type.ofType as? GraphQLNamedType { - return ["Expected \"\(namedType.name)!\", found null."] - } - + if let nonNullType = type as? GraphQLNonNull { + guard let wrappedType = nonNullType.ofType as? GraphQLInputType else { + throw GraphQLError(message: "Input non-null type must wrap another input type") + } + + if value == .null{ return ["Expected non-null value, found null."] } + if value == .undefined { + return ["Expected non-null value was not provided."] + } - return try isValidValue(value: value, type: type.ofType as! GraphQLInputType) + return try validate(value: value, forType: wrappedType) } - - guard value != .null else { + + // If nullable, either null or undefined are allowed + guard value != .null && value != .undefined else { return [] } // Lists accept a non-list value as a list of one. - if let type = type as? GraphQLList { - let itemType = type.ofType + if let listType = type as? GraphQLList { + guard let itemType = listType.ofType as? GraphQLInputType else { + throw GraphQLError(message: "Input list type must wrap another input type") + } if case .array(let values) = value { var errors: [String] = [] for (index, item) in values.enumerated() { - let e = try isValidValue(value: item, type: itemType as! GraphQLInputType).map { + let e = try validate(value: item, forType: itemType).map { "In element #\(index): \($0)" } errors.append(contentsOf: e) @@ -38,16 +44,16 @@ func isValidValue(value: Map, type: GraphQLInputType) throws -> [String] { return errors } - return try isValidValue(value: value, type: itemType as! GraphQLInputType) + return try validate(value: value, forType: itemType) } // Input objects check each defined field. - if let type = type as? GraphQLInputObjectType { + if let objectType = type as? GraphQLInputObjectType { guard case .dictionary(let dictionary) = value else { - return ["Expected \"\(type.name)\", found not an object."] + return ["Expected \"\(objectType.name)\", found not an object."] } - let fields = type.fields + let fields = objectType.fields var errors: [String] = [] // Ensure every provided field is defined. @@ -59,7 +65,7 @@ func isValidValue(value: Map, type: GraphQLInputType) throws -> [String] { // Ensure every defined field is valid. for (fieldName, field) in fields { - let newErrors = try isValidValue(value: value[fieldName], type: field.type).map { + let newErrors = try validate(value: value[fieldName], forType: field.type).map { "In field \"\(fieldName)\": \($0)" } @@ -69,20 +75,20 @@ func isValidValue(value: Map, type: GraphQLInputType) throws -> [String] { return errors } - guard let type = type as? GraphQLLeafType else { - fatalError("Must be input type") - } - - // Scalar/Enum input checks to ensure the type can parse the value to - // a non-null value. - do { - let parseResult = try type.parseValue(value: value) - if parseResult == .null { - return ["Expected type \"\(type.name)\", found \(value)."] + if let leafType = type as? GraphQLLeafType { + // Scalar/Enum input checks to ensure the type can parse the value to + // a non-null value. + do { + let parseResult = try leafType.parseValue(value: value) + if parseResult == .null || parseResult == .undefined { + return ["Expected type \"\(leafType.name)\", found \(value)."] + } + } catch { + return ["Expected type \"\(leafType.name)\", found \(value)."] } - } catch { - return ["Expected type \"\(type.name)\", found \(value)."] + + return [] } - return [] + throw GraphQLError(message: "Provided type was not provided") } diff --git a/Sources/GraphQL/Utilities/ValueFromAST.swift b/Sources/GraphQL/Utilities/ValueFromAST.swift index 811722a6..6e92be72 100644 --- a/Sources/GraphQL/Utilities/ValueFromAST.swift +++ b/Sources/GraphQL/Utilities/ValueFromAST.swift @@ -18,11 +18,14 @@ import OrderedCollections * */ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: Map] = [:]) throws -> Map { - if let nonNullType = type as? GraphQLNonNull { + if let nonNull = type as? GraphQLNonNull { // Note: we're not checking that the result of valueFromAST is non-null. // We're assuming that this query has been validated and the value used // here is of the correct type. - return try valueFromAST(valueAST: valueAST, type: nonNullType.ofType as! GraphQLInputType, variables: variables) + guard let nonNullType = nonNull.ofType as? GraphQLInputType else { + throw GraphQLError(message: "NonNull must wrap an input type") + } + return try valueFromAST(valueAST: valueAST, type: nonNullType, variables: variables) } if let variable = valueAST as? Variable { @@ -37,61 +40,67 @@ func valueFromAST(valueAST: Value, type: GraphQLInputType, variables: [String: M if let variable = variables[variableName] { return variable } else { - return .null + return .undefined } } if let list = type as? GraphQLList { - let itemType = list.ofType + guard let itemType = list.ofType as? GraphQLInputType else { + throw GraphQLError(message: "Input list must wrap an input type") + } if let listValue = valueAST as? ListValue { - return try .array(listValue.values.map({ + let values = try listValue.values.map { item in try valueFromAST( - valueAST: $0, - type: itemType as! GraphQLInputType, + valueAST: item, + type: itemType, variables: variables ) - })) + } + return .array(values) } - - return try [valueFromAST(valueAST: valueAST, type: itemType as! GraphQLInputType, variables: variables)] + + // Convert solitary value into single-value array + return .array([ + try valueFromAST( + valueAST: valueAST, + type: itemType, + variables: variables + ) + ]) } if let objectType = type as? GraphQLInputObjectType { guard let objectValue = valueAST as? ObjectValue else { - throw GraphQLError(message: "Must be object type") + throw GraphQLError(message: "Input object must be object type") } let fields = objectType.fields let fieldASTs = objectValue.fields.keyMap({ $0.name.value }) - - return try .dictionary(fields.keys.reduce(OrderedDictionary()) { obj, fieldName in - var obj = obj - let field = fields[fieldName]! + + var object = OrderedDictionary() + for (fieldName, field) in fields { if let fieldAST = fieldASTs[fieldName] { - let fieldValue = try valueFromAST( + object[fieldName] = try valueFromAST( valueAST: fieldAST.value, type: field.type, variables: variables ) - obj[fieldName] = fieldValue } else { // If AST doesn't contain field, it is undefined if let defaultValue = field.defaultValue { - obj[fieldName] = defaultValue + object[fieldName] = defaultValue } else { - obj[fieldName] = .undefined + object[fieldName] = .undefined } } - - return obj - }) + } + return .dictionary(object) } - guard let type = type as? GraphQLLeafType else { - throw GraphQLError(message: "Must be leaf type") + if let leafType = type as? GraphQLLeafType { + return try leafType.parseLiteral(valueAST: valueAST) } - // If we've made it this far, it should be a literal - return try type.parseLiteral(valueAST: valueAST) + throw GraphQLError(message: "Provided type is not an input type") } diff --git a/Tests/GraphQLTests/InputTests/InputTests.swift b/Tests/GraphQLTests/InputTests/InputTests.swift index c7d9f805..c9969973 100644 --- a/Tests/GraphQLTests/InputTests/InputTests.swift +++ b/Tests/GraphQLTests/InputTests/InputTests.swift @@ -5,8 +5,756 @@ import NIO class InputTests : XCTestCase { + func testArgsNonNullNoDefault() throws { + struct Echo : Codable { + let field1: String + } + + struct EchoArgs : Codable { + let field1: String + } + + let EchoOutputType = try! GraphQLObjectType( + name: "Echo", + description: "", + fields: [ + "field1": GraphQLField( + type: GraphQLNonNull(GraphQLString) + ), + ], + isTypeOf: { source, _, _ in + source is Echo + } + ) + + let schema = try! GraphQLSchema( + query: try! GraphQLObjectType( + name: "Query", + fields: [ + "echo": GraphQLField( + type: EchoOutputType, + args: [ + "field1": GraphQLArgument( + type: GraphQLNonNull(GraphQLString) + ) + ], + resolve: { _, arguments, _, _ in + let args = try MapDecoder().decode(EchoArgs.self, from: arguments) + return Echo( + field1: args.field1 + ) + } + ), + ] + ), + types: [EchoOutputType] + ) + + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + // Test basic functionality + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": "value1" + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + + // Test providing null results in an error + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait() + .errors.count > 0 + ) + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": .null + ] + ).wait() + .errors.count > 0 + ) + + // Test not providing parameter results in an error + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + { + echo { + field1 + } + } + """, + eventLoopGroup: group + ).wait() + .errors.count > 0 + ) + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [:] + ).wait() + .errors.count > 0 + ) + } + + func testArgsNullNoDefault() throws { + struct Echo : Codable { + let field1: String? + } + + struct EchoArgs : Codable { + let field1: String? + } + + let EchoOutputType = try! GraphQLObjectType( + name: "Echo", + description: "", + fields: [ + "field1": GraphQLField( + type: GraphQLString + ), + ], + isTypeOf: { source, _, _ in + source is Echo + } + ) + + let schema = try! GraphQLSchema( + query: try! GraphQLObjectType( + name: "Query", + fields: [ + "echo": GraphQLField( + type: EchoOutputType, + args: [ + "field1": GraphQLArgument( + type: GraphQLString + ) + ], + resolve: { _, arguments, _, _ in + let args = try MapDecoder().decode(EchoArgs.self, from: arguments) + return Echo( + field1: args.field1 + ) + } + ), + ] + ), + types: [EchoOutputType] + ) + + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + // Test basic functionality + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": "value1" + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + + // Test providing null is accepted + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": .null + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + + // Test not providing parameter is accepted + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [:] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + } + + func testArgsNonNullDefault() throws { + struct Echo : Codable { + let field1: String + } + + struct EchoArgs : Codable { + let field1: String + } + + let EchoOutputType = try! GraphQLObjectType( + name: "Echo", + description: "", + fields: [ + "field1": GraphQLField( + type: GraphQLNonNull(GraphQLString) + ), + ], + isTypeOf: { source, _, _ in + source is Echo + } + ) + + let schema = try! GraphQLSchema( + query: try! GraphQLObjectType( + name: "Query", + fields: [ + "echo": GraphQLField( + type: EchoOutputType, + args: [ + "field1": GraphQLArgument( + type: GraphQLNonNull(GraphQLString), + defaultValue: .string("defaultValue1") + ) + ], + resolve: { _, arguments, _, _ in + let args = try MapDecoder().decode(EchoArgs.self, from: arguments) + return Echo( + field1: args.field1 + ) + } + ), + ] + ), + types: [EchoOutputType] + ) + + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + // Test basic functionality + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": "value1" + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + + // Test providing null results in an error + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait() + .errors.count > 0 + ) + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": .null + ] + ).wait() + .errors.count > 0 + ) + + // Test not providing parameter results in default + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "defaultValue1" + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String! = "defaultValue1") { + echo ( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [:] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "defaultValue1" + ] + ]) + ) + + // Test variable doesn't get argument default + XCTAssertTrue( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo ( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [:] + ).wait() + .errors.count > 0 + ) + } + + func testArgsNullDefault() throws { + struct Echo : Codable { + let field1: String? + } + + struct EchoArgs : Codable { + let field1: String? + } + + let EchoOutputType = try! GraphQLObjectType( + name: "Echo", + description: "", + fields: [ + "field1": GraphQLField( + type: GraphQLString + ), + ], + isTypeOf: { source, _, _ in + source is Echo + } + ) + + let schema = try! GraphQLSchema( + query: try! GraphQLObjectType( + name: "Query", + fields: [ + "echo": GraphQLField( + type: EchoOutputType, + args: [ + "field1": GraphQLArgument( + type: GraphQLString, + defaultValue: .string("defaultValue1") + ) + ], + resolve: { _, arguments, _, _ in + let args = try MapDecoder().decode(EchoArgs.self, from: arguments) + return Echo( + field1: args.field1 + ) + } + ), + ] + ), + types: [EchoOutputType] + ) + + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } + + // Test basic functionality + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": "value1" + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1" + ] + ]) + ) + + // Test providing null results in a null output + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "field1": .null + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + + // Test not providing parameter results in default + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo { + field1 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "defaultValue1" + ] + ]) + ) + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String = "defaultValue1") { + echo ( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [:] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "defaultValue1" + ] + ]) + ) + + // Test that nullable unprovided variables are coerced to null + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo ( + field1: $field1 + ) { + field1 + } + } + """, + eventLoopGroup: group, + variableValues: [:] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": .null + ] + ]) + ) + } + // Test that input objects parse as expected from non-null literals - func testInputParsing() throws { + func testInputNoNull() throws { struct Echo : Codable { let field1: String? let field2: String? @@ -86,6 +834,7 @@ class InputTests : XCTestCase { XCTAssertNoThrow(try group.syncShutdownGracefully()) } + // Test in arguments XCTAssertEqual( try graphql( schema: schema, @@ -109,6 +858,34 @@ class InputTests : XCTestCase { ] ]) ) + + // Test in variables + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "input": [ + "field1": "value1", + "field2": "value2" + ] + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1", + "field2": "value2", + ] + ]) + ) } // Test that inputs parse as expected when null literals are present @@ -192,6 +969,7 @@ class InputTests : XCTestCase { XCTAssertNoThrow(try group.syncShutdownGracefully()) } + // Test in arguments XCTAssertEqual( try graphql( schema: schema, @@ -215,6 +993,34 @@ class InputTests : XCTestCase { ] ]) ) + + // Test in variables + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "input": [ + "field1": "value1", + "field2": .null + ] + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1", + "field2": nil, + ] + ]) + ) } // Test that input objects parse as expected when there are missing fields with no default @@ -298,6 +1104,7 @@ class InputTests : XCTestCase { XCTAssertNoThrow(try group.syncShutdownGracefully()) } + // Test in arguments XCTAssertEqual( try graphql( schema: schema, @@ -320,6 +1127,33 @@ class InputTests : XCTestCase { ] ]) ) + + // Test in variables + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "input": [ + "field1": "value1" + ] + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1", + "field2": nil, + ] + ]) + ) } // Test that input objects parse as expected when there are missing fields with defaults @@ -404,6 +1238,30 @@ class InputTests : XCTestCase { XCTAssertNoThrow(try group.syncShutdownGracefully()) } + // Undefined with default gets default + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + }) { + field1 + field2 + } + } + """, + eventLoopGroup: group + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1", + "field2": "value2", + ] + ]) + ) + // Null literal with default gets null XCTAssertEqual( try graphql( schema: schema, @@ -411,6 +1269,7 @@ class InputTests : XCTestCase { { echo(input:{ field1: "value1" + field2: null }) { field1 field2 @@ -419,6 +1278,34 @@ class InputTests : XCTestCase { """, eventLoopGroup: group ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1", + "field2": nil, + ] + ]) + ) + + // Test in variable + // Undefined with default gets default + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "input": [ + "field1": "value1" + ] + ] + ).wait(), GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -426,5 +1313,32 @@ class InputTests : XCTestCase { ] ]) ) + // Null literal with default gets null + XCTAssertEqual( + try graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 + } + } + """, + eventLoopGroup: group, + variableValues: [ + "input": [ + "field1": "value1", + "field2": .null + ] + ] + ).wait(), + GraphQLResult(data: [ + "echo": [ + "field1": "value1", + "field2": nil, + ] + ]) + ) } }