diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index c69e84c543..81b6a308ce 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -180,7 +180,7 @@ DE05860B266978A100265760 /* SelectionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */; }; DE05860C266978A100265760 /* FragmentProtocols.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B12260A6FC900D2F4FF /* FragmentProtocols.swift */; }; DE05860D266978A100265760 /* ScalarTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B15260A6FCA00D2F4FF /* ScalarTypes.swift */; }; - DE05860E266978A100265760 /* GraphQLOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */; }; + DE05860E266978A100265760 /* GraphQLNullable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F06E241C649E00E97318 /* GraphQLNullable.swift */; }; DE058610266978A100265760 /* InputValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9C11E2D3CAF0023C4D5 /* InputValue.swift */; }; DE058616266978A100265760 /* GraphQLEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B14260A6FCA00D2F4FF /* GraphQLEnum.swift */; }; DE05862D2669800000265760 /* Matchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071AE2368D34D00FA5952 /* Matchable.swift */; }; @@ -226,7 +226,6 @@ DE674D9D261CEEE4000E8FC8 /* c.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9B2061172591B3550020D1E0 /* c.txt */; }; DE674D9E261CEEE4000E8FC8 /* b.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9B2061182591B3550020D1E0 /* b.txt */; }; DE674D9F261CEEE4000E8FC8 /* a.txt in Resources */ = {isa = PBXBuildFile; fileRef = 9B2061192591B3550020D1E0 /* a.txt */; }; - DE6B156A261505660068D642 /* GraphQLMap.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6B154A261505450068D642 /* GraphQLMap.swift */; }; DE6B15AF26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE6B15AE26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift */; }; DE6B15B126152BE10068D642 /* Apollo.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9FC750441D2A532C00458D91 /* Apollo.framework */; }; DE736F4626FA6EE6007187F2 /* InflectorKit in Frameworks */ = {isa = PBXBuildFile; productRef = E6E4209126A7DF4200B82624 /* InflectorKit */; }; @@ -627,7 +626,7 @@ 9B68353E2463481A00337AE6 /* ApolloUtils.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ApolloUtils.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9B68354A2463498D00337AE6 /* Apollo-Target-ApolloUtils.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = "Apollo-Target-ApolloUtils.xcconfig"; sourceTree = ""; }; 9B68F0542416B33300E97318 /* LineByLineComparison.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LineByLineComparison.swift; sourceTree = ""; }; - 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLOptional.swift; sourceTree = ""; }; + 9B68F06E241C649E00E97318 /* GraphQLNullable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLNullable.swift; sourceTree = ""; }; 9B6CB23D238077B60007259D /* Atomic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; 9B708AAC2305884500604A11 /* ApolloClientProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApolloClientProtocol.swift; sourceTree = ""; }; 9B74BCBE2333F4ED00508F84 /* run-bundled-codegen.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; name = "run-bundled-codegen.sh"; path = "scripts/run-bundled-codegen.sh"; sourceTree = SOURCE_ROOT; }; @@ -853,7 +852,6 @@ DE5EB9C626EFE0F80004176A /* JSONValueMatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = JSONValueMatcher.swift; sourceTree = ""; }; DE5EB9CA26EFE5510004176A /* MockOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockOperation.swift; sourceTree = ""; }; DE664ED326602AF60054DB4F /* Selection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Selection.swift; sourceTree = ""; }; - DE6B154A261505450068D642 /* GraphQLMap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GraphQLMap.swift; sourceTree = ""; }; DE6B15AC26152BE10068D642 /* ApolloServerIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ApolloServerIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; DE6B15AE26152BE10068D642 /* DefaultInterceptorProviderIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultInterceptorProviderIntegrationTests.swift; sourceTree = ""; }; DE6B15B026152BE10068D642 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -1492,7 +1490,6 @@ children = ( DE9C04AB26EA763E00EC35E7 /* Accumulators */, 9FF90A5C1DDDEB100034C3B6 /* GraphQLExecutor.swift */, - DE6B154A261505450068D642 /* GraphQLMap.swift */, 9FA6F3671E65DF4700BF8D73 /* GraphQLResultAccumulator.swift */, 9F7BA89822927A3700999B3B /* ResponsePath.swift */, DE0586322669948500265760 /* InputValue+Evaluation.swift */, @@ -1729,7 +1726,7 @@ DE2FCF2226E8082A0057EA67 /* SchemaTypes */, DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */, DE9C04AE26EAAEE800EC35E7 /* CacheReference.swift */, - 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */, + 9B68F06E241C649E00E97318 /* GraphQLNullable.swift */, DE3C7B12260A6FC900D2F4FF /* FragmentProtocols.swift */, DE3C7B14260A6FCA00D2F4FF /* GraphQLEnum.swift */, DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */, @@ -2828,7 +2825,6 @@ 9FEB050D1DB5732300DA3B44 /* JSONSerializationFormat.swift in Sources */, 9B260BEB245A020300562176 /* ApolloInterceptor.swift in Sources */, 54DDB0921EA045870009DD99 /* InMemoryNormalizedCache.swift in Sources */, - DE6B156A261505660068D642 /* GraphQLMap.swift in Sources */, 9B554CC4247DC29A002F452A /* TaskData.swift in Sources */, 9B9BBAF524DB4F890021C30F /* AutomaticPersistedQueryInterceptor.swift in Sources */, 9BA1244A22D8A8EA00BF1D24 /* JSONSerialization+Sorting.swift in Sources */, @@ -2946,7 +2942,7 @@ DE2FCF2A26E8083A0057EA67 /* Field.swift in Sources */, DE05860D266978A100265760 /* ScalarTypes.swift in Sources */, DE9C04AF26EAAEE800EC35E7 /* CacheReference.swift in Sources */, - DE05860E266978A100265760 /* GraphQLOptional.swift in Sources */, + DE05860E266978A100265760 /* GraphQLNullable.swift in Sources */, DECD53CF26EC0EE50059A639 /* OutputTypeConvertible.swift in Sources */, DE2FCF2126E807EF0057EA67 /* Cacheable.swift in Sources */, DEA6A83426F298660091AF8A /* ParentType.swift in Sources */, diff --git a/Sources/Apollo/ApolloStore.swift b/Sources/Apollo/ApolloStore.swift index dcbce0268b..9ba8df7433 100644 --- a/Sources/Apollo/ApolloStore.swift +++ b/Sources/Apollo/ApolloStore.swift @@ -184,7 +184,7 @@ public class ApolloStore { public func readObject( ofType type: SelectionSet.Type, withKey key: CacheKey, - variables: [String: InputValue]? = nil + variables: GraphQLOperation.Variables? = nil ) throws -> SelectionSet { return try self.readObject(ofType: type, withKey: key, @@ -195,7 +195,7 @@ public class ApolloStore { func readObject( ofType type: SelectionSet.Type, withKey key: CacheKey, - variables: [String: InputValue]? = nil, + variables: GraphQLOperation.Variables? = nil, accumulator: Accumulator ) throws -> Accumulator.FinalResult { let object = try loadObject(forKey: key).get() diff --git a/Sources/Apollo/GraphQLExecutor.swift b/Sources/Apollo/GraphQLExecutor.swift index d6ca712eaa..9b70e64cb1 100644 --- a/Sources/Apollo/GraphQLExecutor.swift +++ b/Sources/Apollo/GraphQLExecutor.swift @@ -15,13 +15,13 @@ typealias GraphQLFieldResolver = (_ object: JSONObject, _ info: FieldExecutionIn typealias ReferenceResolver = (CacheReference) -> PossiblyDeferred struct ObjectExecutionInfo { - let variables: [String: InputValue]? + let variables: GraphQLOperation.Variables? let schema: SchemaConfiguration.Type private(set) var responsePath: ResponsePath = [] private(set) var cachePath: ResponsePath = [] fileprivate init( - variables: [String: InputValue]?, + variables: GraphQLOperation.Variables?, schema: SchemaConfiguration.Type, responsePath: ResponsePath, cachePath: ResponsePath @@ -33,7 +33,7 @@ struct ObjectExecutionInfo { } fileprivate init( - variables: [String: InputValue]?, + variables: GraphQLOperation.Variables?, schema: SchemaConfiguration.Type, withRootCacheReference root: CacheReference? = nil ) { @@ -173,7 +173,7 @@ final class GraphQLExecutor { selectionSet: RootSelectionSet.Type, on data: JSONObject, withRootCacheReference root: CacheReference? = nil, - variables: [String: InputValue]? = nil, + variables: GraphQLOperation.Variables? = nil, accumulator: Accumulator ) throws -> Accumulator.FinalResult { let info = ObjectExecutionInfo(variables: variables, @@ -247,8 +247,7 @@ final class GraphQLExecutor { groupedFields.append(field: field, withInfo: info) case let .booleanCondition(booleanCondition): - guard case let .scalar(boolValue as Bool) = - info.variables?[booleanCondition.variableName] else { + guard let boolValue = info.variables?[booleanCondition.variableName] as? Bool else { throw GraphQLError("Variable \"\(booleanCondition.variableName)\" was not provided.") } if boolValue == !booleanCondition.inverted { diff --git a/Sources/Apollo/GraphQLGETTransformer.swift b/Sources/Apollo/GraphQLGETTransformer.swift index 5108f1effd..97bd29c031 100644 --- a/Sources/Apollo/GraphQLGETTransformer.swift +++ b/Sources/Apollo/GraphQLGETTransformer.swift @@ -1,19 +1,20 @@ import Foundation #if !COCOAPODS import ApolloUtils +import ApolloAPI #endif public struct GraphQLGETTransformer { - let body: GraphQLMap + let body: JSONEncodableDictionary let url: URL - /// A helper for transforming a GraphQLMap that can be sent with a `POST` request into a URL with query parameters for a `GET` request. + /// A helper for transforming a `JSONEncodableDictionary` that can be sent with a `POST` request into a URL with query parameters for a `GET` request. /// /// - Parameters: - /// - body: The GraphQLMap to transform from the body of a `POST` request + /// - body: The `JSONEncodableDictionary` to transform from the body of a `POST` request /// - url: The base url to append the query to. - public init(body: GraphQLMap, url: URL) { + public init(body: JSONEncodableDictionary, url: URL) { self.body = body self.url = url } @@ -30,7 +31,7 @@ public struct GraphQLGETTransformer { do { _ = try self.body.sorted(by: {$0.key < $1.key}).compactMap({ arg in - if let value = arg.value as? GraphQLMap { + if let value = arg.value as? JSONEncodableDictionary { let data = try JSONSerialization.sortedData(withJSONObject: value.jsonValue) if let string = String(data: data, encoding: .utf8) { queryItems.append(URLQueryItem(name: arg.key, value: string)) diff --git a/Sources/Apollo/GraphQLMap.swift b/Sources/Apollo/GraphQLMap.swift deleted file mode 100644 index 4c2cd3549f..0000000000 --- a/Sources/Apollo/GraphQLMap.swift +++ /dev/null @@ -1,22 +0,0 @@ -#if !COCOAPODS -import ApolloAPI -#endif - -#warning("TODO: Delete?") -public typealias GraphQLMap = [String: JSONEncodable?] - -fileprivate extension Dictionary where Key == String, Value == JSONEncodable? { - var withNilValuesRemoved: Dictionary { - filter { $0.value != nil } - } -} - -public protocol GraphQLMapConvertible: JSONEncodable { - var graphQLMap: GraphQLMap { get } -} - -public extension GraphQLMapConvertible { - var jsonValue: JSONValue { - return graphQLMap.withNilValuesRemoved.jsonValue - } -} diff --git a/Sources/Apollo/GraphQLResponse.swift b/Sources/Apollo/GraphQLResponse.swift index ce55388594..bc57ff5541 100644 --- a/Sources/Apollo/GraphQLResponse.swift +++ b/Sources/Apollo/GraphQLResponse.swift @@ -8,7 +8,7 @@ public final class GraphQLResponse { public let body: JSONObject private let rootKey: CacheReference - private let variables: [String: InputValue]? + private let variables: GraphQLOperation.Variables? public init(operation: Operation, body: JSONObject) where Operation.Data == Data { self.body = body diff --git a/Sources/Apollo/InputValue+Evaluation.swift b/Sources/Apollo/InputValue+Evaluation.swift index 055175f3de..f233c837b2 100644 --- a/Sources/Apollo/InputValue+Evaluation.swift +++ b/Sources/Apollo/InputValue+Evaluation.swift @@ -4,12 +4,12 @@ import ApolloAPI import Foundation extension Selection.Field { - func cacheKey(with variables: [String: InputValue]?) throws -> String { - if - let argumentValues = try arguments?.evaluate(with: variables), - argumentValues.apollo.isNotEmpty { - let argumentsKey = orderIndependentKey(for: argumentValues) - return "\(name)(\(argumentsKey))" + func cacheKey(with variables: GraphQLOperation.Variables?) throws -> String { + if let arguments = arguments, + case let argumentValues = try InputValue.evaluate(arguments, with: variables), + argumentValues.apollo.isNotEmpty { + let argumentsKey = orderIndependentKey(for: argumentValues) + return "\(name)(\(argumentsKey))" } else { return name } @@ -17,67 +17,54 @@ extension Selection.Field { private func orderIndependentKey(for object: JSONObject) -> String { return object.sorted { $0.key < $1.key }.map { - if let object = $0.value as? JSONObject { + switch $0.value { + case let object as JSONObject: return "[\($0.key):\(orderIndependentKey(for: object))]" - } else if let array = $0.value as? [JSONObject] { + case let array as [JSONObject]: return "\($0.key):[\(array.map { orderIndependentKey(for: $0) }.joined(separator: ","))]" - } else { + case is NSNull: + return "\($0.key):null" + default: return "\($0.key):\($0.value)" } }.joined(separator: ",") } } -extension Selection.Field.Arguments { - - func evaluate(with variables: [String: InputValue]?) throws -> JSONObject { - /// `Selection.Field.Arguments` can only ever hold arguments of `.object` type. Which will - /// always resolve to an `.object` value. So force casting to `JSONObject` is safe. - return try arguments.evaluate(with: variables) as! JSONObject - } -} - extension InputValue { - func evaluate(with variables: [String: InputValue]?) throws -> JSONValue { + private func evaluate(with variables: GraphQLOperation.Variables?) throws -> JSONValue? { switch self { - case let .scalar(value): - return value - case let .variable(name): guard let value = variables?[name] else { throw GraphQLError("Variable \"\(name)\" was not provided.") } + return value.jsonEncodableValue?.jsonValue - switch value { - case let .variable(nestedName) where name == nestedName: - throw GraphQLError("Variable \"\(name)\" is infinitely recursive.") - default: - return try value.evaluate(with: variables) - } + case let .scalar(value): + return value case let .list(array): - return try evaluate(values: array, with: variables) + return try InputValue.evaluate(array, with: variables) case let .object(dictionary): - return try evaluate(values: dictionary, with: variables) + return try InputValue.evaluate(dictionary, with: variables) - case .none: + case .null: return NSNull() } } - private func evaluate(values: [InputValue], with variables: [String: InputValue]?) throws -> [JSONValue] { - try values.map { try $0.evaluate(with: variables) } + fileprivate static func evaluate( + _ values: [InputValue], + with variables: GraphQLOperation.Variables? + ) throws -> [JSONValue] { + try values.compactMap { try $0.evaluate(with: variables) } } - private func evaluate(values: [String: InputValue], with variables: [String: InputValue]?) throws -> JSONObject { - var jsonObject = JSONObject(minimumCapacity: values.count) - for (key, value) in values { - let evaluatedValue = try value.evaluate(with: variables) - if !(evaluatedValue is NSNull) { - jsonObject[key] = evaluatedValue - } - } - return jsonObject + fileprivate static func evaluate( + _ values: [String: InputValue], + with variables: GraphQLOperation.Variables? + ) throws -> JSONObject { + try values.compactMapValues { try $0.evaluate(with: variables) } } } diff --git a/Sources/Apollo/JSONSerializationFormat.swift b/Sources/Apollo/JSONSerializationFormat.swift index 156b57556b..077d86b6cd 100644 --- a/Sources/Apollo/JSONSerializationFormat.swift +++ b/Sources/Apollo/JSONSerializationFormat.swift @@ -11,6 +11,10 @@ public final class JSONSerializationFormat { return try JSONSerialization.sortedData(withJSONObject: value.jsonValue) } + public class func serialize(value: JSONObject) throws -> Data { + return try JSONSerialization.sortedData(withJSONObject: value) + } + public class func deserialize(data: Data) throws -> JSONValue { return try JSONSerialization.jsonObject(with: data, options: []) } diff --git a/Sources/Apollo/RequestBodyCreator.swift b/Sources/Apollo/RequestBodyCreator.swift index 164a6c73a8..9d1cc66287 100644 --- a/Sources/Apollo/RequestBodyCreator.swift +++ b/Sources/Apollo/RequestBodyCreator.swift @@ -13,25 +13,32 @@ public protocol RequestBodyCreator { /// - sendQueryDocument: Whether or not to send the full query document. Should default to `true`. /// - autoPersistQuery: Whether to use auto-persisted query information. Should default to `false`. /// - Returns: The created `GraphQLMap` - func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool, - sendQueryDocument: Bool, - autoPersistQuery: Bool) -> GraphQLMap + func requestBody( + for operation: Operation, + sendOperationIdentifiers: Bool, + sendQueryDocument: Bool, + autoPersistQuery: Bool + ) -> JSONEncodableDictionary } // MARK: - Default Implementation extension RequestBodyCreator { - public func requestBody(for operation: Operation, - sendOperationIdentifiers: Bool, - sendQueryDocument: Bool, - autoPersistQuery: Bool) -> GraphQLMap { - var body: GraphQLMap = [ - "variables": operation.variables?.jsonObject, + public func requestBody( + for operation: Operation, + sendOperationIdentifiers: Bool, + sendQueryDocument: Bool, + autoPersistQuery: Bool + ) -> JSONEncodableDictionary { + var body: JSONEncodableDictionary = [ "operationName": operation.operationName, ] + if let variables = operation.variables { + body["variables"] = variables.jsonEncodableObject + } + if sendOperationIdentifiers { guard let operationIdentifier = operation.operationIdentifier else { preconditionFailure("To send operation identifiers, Apollo types must be generated with operationIdentifiers") diff --git a/Sources/Apollo/UploadRequest.swift b/Sources/Apollo/UploadRequest.swift index d15f3ad0e3..a2767e63a4 100644 --- a/Sources/Apollo/UploadRequest.swift +++ b/Sources/Apollo/UploadRequest.swift @@ -75,15 +75,15 @@ open class UploadRequest: HTTPRequest { sendOperationIdentifiers: shouldSendOperationID, sendQueryDocument: true, autoPersistQuery: false) - var variables = fields["variables"] as? GraphQLMap ?? GraphQLMap() + var variables = fields["variables"] as? JSONEncodableDictionary ?? JSONEncodableDictionary() for fieldName in fieldsForFiles { if let value = variables[fieldName], let arrayValue = value as? [JSONEncodable] { - let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in nil } + let arrayOfNils: [JSONEncodable?] = arrayValue.map { _ in NSNull() } variables.updateValue(arrayOfNils, forKey: fieldName) } else { - variables.updateValue(nil, forKey: fieldName) + variables.updateValue(NSNull(), forKey: fieldName) } } fields["variables"] = variables diff --git a/Sources/ApolloAPI/GraphQLEnum.swift b/Sources/ApolloAPI/GraphQLEnum.swift index 6ef3e7b22e..3e794eb84b 100644 --- a/Sources/ApolloAPI/GraphQLEnum.swift +++ b/Sources/ApolloAPI/GraphQLEnum.swift @@ -1,10 +1,18 @@ +/// A protocol that a generated enum from a GraphQL schema conforms to. +/// This allows it to be wrapped in a `GraphQLEnum` and be used as an input value for operations. +public protocol EnumType: + RawRepresentable, + CaseIterable, + JSONEncodable, + GraphQLOperationVariableValue +where RawValue == String {} + /// A generic enum that wraps a generated enum from a GraphQL Schema. /// /// `GraphQLEnum` provides an `__unknown` case that is used when the response returns a value that /// is not recognized as a valid enum case. This is usually caused by future cases added to the enum /// on the schema after code generation. -public enum GraphQLEnum: CaseIterable, Equatable, RawRepresentable -where T: RawRepresentable & CaseIterable, T.RawValue == String { +public enum GraphQLEnum: CaseIterable, Equatable, RawRepresentable { public typealias RawValue = String /// A recognized case of the wrapped enum. diff --git a/Sources/ApolloAPI/GraphQLNullable.swift b/Sources/ApolloAPI/GraphQLNullable.swift new file mode 100644 index 0000000000..8cd0b1701b --- /dev/null +++ b/Sources/ApolloAPI/GraphQLNullable.swift @@ -0,0 +1,34 @@ +import Foundation + +@dynamicMemberLookup +public enum GraphQLNullable: ExpressibleByNilLiteral { + + /// The absence of a value. + /// Functionally equivalent to `nil`. + case none + + /// The presence of an explicity null value. + /// Functionally equivalent to `NSNull` + case null + + /// The presence of a value, stored as `Wrapped` + case some(Wrapped) + + public var unwrapped: Wrapped? { + guard case let .some(wrapped) = self else { return nil } + return wrapped + } + + public var unsafelyUnwrapped: Wrapped { + guard case let .some(wrapped) = self else { fatalError("Force unwrap Nullable value failed!") } + return wrapped + } + + public subscript(dynamicMember path: KeyPath) -> T? { + unwrapped?[keyPath: path] + } + + public init(nilLiteral: ()) { + self = .none + } +} diff --git a/Sources/ApolloAPI/GraphQLOperation.swift b/Sources/ApolloAPI/GraphQLOperation.swift index 44e6faa3f6..03661b3f8d 100644 --- a/Sources/ApolloAPI/GraphQLOperation.swift +++ b/Sources/ApolloAPI/GraphQLOperation.swift @@ -1,3 +1,5 @@ +import Foundation + public enum GraphQLOperationType { case query case mutation @@ -5,6 +7,8 @@ public enum GraphQLOperationType { } public protocol GraphQLOperation: AnyObject { + typealias Variables = [String: GraphQLOperationVariableValue] + var operationType: GraphQLOperationType { get } var operationDefinition: String { get } @@ -13,8 +17,7 @@ public protocol GraphQLOperation: AnyObject { var queryDocument: String { get } -#warning("TODO: We need to support setting a null value AND a nil value. Considering just going back to using GraphQLMap, or else this should be [String: GraphQLOptional].") - var variables: [String: InputValue]? { get } + var variables: Variables? { get } associatedtype Data: RootSelectionSet } @@ -28,7 +31,7 @@ public extension GraphQLOperation { return nil } - var variables: [String: InputValue]? { + var variables: Variables? { return nil } } @@ -47,3 +50,32 @@ public protocol GraphQLSubscription: GraphQLOperation {} public extension GraphQLSubscription { var operationType: GraphQLOperationType { return .subscription } } + +// MARK: - GraphQLOperationVariableValue + +public protocol GraphQLOperationVariableValue { + var jsonEncodableValue: JSONEncodable? { get } +} + +extension Array: GraphQLOperationVariableValue where Element: GraphQLOperationVariableValue {} + +extension Dictionary: GraphQLOperationVariableValue where Key == String, Value == GraphQLOperationVariableValue { + public var jsonEncodableValue: JSONEncodable? { jsonEncodableObject } + public var jsonEncodableObject: JSONEncodableDictionary { + compactMapValues { $0.jsonEncodableValue } + } +} + +extension GraphQLNullable: GraphQLOperationVariableValue where Wrapped: JSONEncodable { + public var jsonEncodableValue: JSONEncodable? { + switch self { + case .none: return nil + case .null: return NSNull() + case let .some(value): return value + } + } +} + +extension JSONEncodable where Self: GraphQLOperationVariableValue { + public var jsonEncodableValue: JSONEncodable? { self } +} diff --git a/Sources/ApolloAPI/GraphQLOptional.swift b/Sources/ApolloAPI/GraphQLOptional.swift deleted file mode 100644 index 2b783d4f3e..0000000000 --- a/Sources/ApolloAPI/GraphQLOptional.swift +++ /dev/null @@ -1,64 +0,0 @@ -import Foundation - -public enum GraphQLOptional { - case notPresent - case nullValue - case value(T) -} - -extension GraphQLOptional: Hashable where T: Hashable { - - public func hash(into hasher: inout Hasher) { - switch self { - case .notPresent, - .nullValue: - // no-op - break - case .value(let hashableType): - hashableType.hash(into: &hasher) - } - } -} - -extension GraphQLOptional: Equatable where T: Equatable { - public static func ==(lhs: GraphQLOptional, rhs: GraphQLOptional) -> Bool { - switch (lhs, rhs) { - case (.notPresent, .notPresent), - (.nullValue, .nullValue): - return true - case (.value(let lhsValue), .value(let rhsValue)): - return lhsValue == rhsValue - default: - return false - } - } -} - -public extension KeyedEncodingContainer { - - mutating func encodeGraphQLOptional(_ optional: GraphQLOptional, forKey key: K) throws { - switch optional { - case .notPresent: - break - case .nullValue: - try self.encodeNil(forKey: key) - case .value(let value): - try self.encode(value, forKey: key) - } - } -} - -public extension KeyedDecodingContainer { - - func decodeGraphQLOptional(forKey key: K) throws -> GraphQLOptional { - if self.contains(key) { - if let value = try? self.decode(T.self, forKey: key) { - return .value(value) - } else { - return .nullValue - } - } else { - return .notPresent - } - } -} diff --git a/Sources/ApolloAPI/InputValue.swift b/Sources/ApolloAPI/InputValue.swift index dbb0ea4fa2..9e77ac0bd2 100644 --- a/Sources/ApolloAPI/InputValue.swift +++ b/Sources/ApolloAPI/InputValue.swift @@ -1,7 +1,7 @@ import Foundation #warning("TODO: It might be more performant to just use the raw values like before commit: e7a9b2c27f9d01764943f1aa7ff9d759d8762bff - We should run performance tests and try reverting and see what performance is like.") -/// Represents an input value to an argument on a `GraphQLField`'s `FieldArguments`. +/// Represents an input value to an argument on a `Selection.Field`'s `Arguments`. /// /// - See: [GraphQLSpec - Input Values](http://spec.graphql.org/June2018/#sec-Input-Values) public indirect enum InputValue { @@ -24,22 +24,10 @@ public indirect enum InputValue { case object([String: InputValue]) /// A null input value. + /// + /// A null input value indicates an intentional inclusion of a value for a field argument as null. /// - See: [GraphQLSpec - Input Values - Null Value](http://spec.graphql.org/June2018/#sec-Null-Value) - case none -} - -// MARK: - JSONEncodable - -extension InputValue: JSONEncodable { - public var jsonValue: JSONValue { - switch self { - case let .scalar(value): return value - case let .variable(variableName): return "$\(variableName)" - case let .list(values): return values.map { $0.jsonValue } - case let .object(valueObject): return valueObject.mapValues { $0.jsonValue } - case .none: return NSNull() - } - } + case null } // MARK: - InputValueConvertible @@ -68,21 +56,7 @@ extension InputValueConvertible where Self: RawRepresentable, RawValue == String @inlinable public var asInputValue: InputValue { .scalar(rawValue) } } -extension Optional: InputValueConvertible where Wrapped: InputValueConvertible { - @inlinable public var asInputValue: InputValue { - switch self { - case .none: return .none - case let .some(value): return value.asInputValue - } - } -} - // MARK: - Expressible as literals -extension InputValue: ExpressibleByNilLiteral { - @inlinable public init(nilLiteral: ()) { - self = .none - } -} extension InputValue: ExpressibleByStringLiteral { @inlinable public init(stringLiteral value: StringLiteralType) { @@ -121,14 +95,6 @@ extension InputValue: ExpressibleByDictionaryLiteral { } } -// MARK = Variable Dictionary Conversion - -extension Dictionary where Key == String, Value == InputValueConvertible { - @inlinable public func toInputVariables() -> [String: InputValue] { - mapValues { $0.asInputValue } - } -} - // MARK: Equatable Conformance extension InputValue: Equatable { @@ -149,7 +115,7 @@ extension InputValue: Equatable { return lhsValue.elementsEqual(rhsValue) case let (.object(lhsValue), .object(rhsValue)): return lhsValue.elementsEqual(rhsValue, by: { $0.key == $1.key && $0.value == $1.value }) - case (.none, .none): + case (.null, .null): return true default: return false } diff --git a/Sources/ApolloAPI/JSON.swift b/Sources/ApolloAPI/JSON.swift index a01fc064f8..16e7364894 100644 --- a/Sources/ApolloAPI/JSON.swift +++ b/Sources/ApolloAPI/JSON.swift @@ -2,6 +2,7 @@ import Foundation public typealias JSONValue = Any public typealias JSONObject = [String: JSONValue] +public typealias JSONEncodableDictionary = [String: JSONEncodable] public protocol JSONDecodable { init(jsonValue value: JSONValue) throws diff --git a/Sources/ApolloAPI/JSONStandardTypeConversions.swift b/Sources/ApolloAPI/JSONStandardTypeConversions.swift index 2ead5a9a63..ad1db2380f 100644 --- a/Sources/ApolloAPI/JSONStandardTypeConversions.swift +++ b/Sources/ApolloAPI/JSONStandardTypeConversions.swift @@ -69,6 +69,11 @@ extension Bool: JSONDecodable, JSONEncodable { } } +extension EnumType { + public var jsonValue: JSONValue { rawValue } +} + +#warning("might be able to remove these since we are using EnumType") extension RawRepresentable where RawValue: JSONDecodable { public init(jsonValue value: JSONValue) throws { let rawValue = try RawValue(jsonValue: value) @@ -119,21 +124,13 @@ extension NSNull: JSONEncodable { public var jsonValue: JSONValue { self } } -extension Dictionary: JSONEncodable { +extension JSONEncodableDictionary: JSONEncodable { public var jsonValue: JSONValue { return jsonObject } public var jsonObject: JSONObject { - var jsonObject = JSONObject(minimumCapacity: count) - for (key, value) in self { - if case let (key as String, value as JSONEncodable) = (key, value) { - jsonObject[key] = value.jsonValue - } else { - fatalError("Dictionary is only JSONEncodable if Value is (and if Key is String)") - } - } - return jsonObject + mapValues(\.jsonValue) } } diff --git a/Sources/ApolloAPI/ScalarTypes.swift b/Sources/ApolloAPI/ScalarTypes.swift index 8a36a09cdb..90b9441a83 100644 --- a/Sources/ApolloAPI/ScalarTypes.swift +++ b/Sources/ApolloAPI/ScalarTypes.swift @@ -5,7 +5,8 @@ public protocol ScalarType: JSONDecodable, JSONEncodable, Cacheable, - InputValueConvertible {} + InputValueConvertible, + GraphQLOperationVariableValue {} extension String: ScalarType {} extension Int: ScalarType {} diff --git a/Sources/ApolloAPI/Selection.swift b/Sources/ApolloAPI/Selection.swift index ed264ffd20..ed1b9e9a2b 100644 --- a/Sources/ApolloAPI/Selection.swift +++ b/Sources/ApolloAPI/Selection.swift @@ -9,8 +9,7 @@ public enum Selection { public struct Field { public let name: String public let alias: String? - #warning("TODO: can we just change this to [String: InputValue] and kill Arguments?") - public let arguments: Arguments? + public let arguments: [String: InputValue]? public let type: OutputType public var responseKey: String { @@ -21,7 +20,7 @@ public enum Selection { _ name: String, alias: String? = nil, type: OutputType, - arguments: Arguments? = nil + arguments: [String: InputValue]? = nil ) { self.name = name self.alias = alias @@ -31,14 +30,6 @@ public enum Selection { self.type = type } - public struct Arguments: ExpressibleByDictionaryLiteral { - public let arguments: InputValue - - @inlinable public init(dictionaryLiteral elements: (String, InputValue)...) { - arguments = .object(Dictionary(elements, uniquingKeysWith: { (_, last) in last })) - } - } - public indirect enum OutputType { case scalar(ScalarType.Type) case customScalar(CustomScalarType.Type) @@ -82,7 +73,7 @@ public enum Selection { _ name: String, alias: String? = nil, _ type: OutputTypeConvertible.Type, - arguments: Field.Arguments? = nil + arguments: [String: InputValue]? = nil ) -> Selection { .field(.init(name, alias: alias, type: type.asOutputType, arguments: arguments)) } diff --git a/Sources/ApolloTestSupport/MockOperation.swift b/Sources/ApolloTestSupport/MockOperation.swift index d473eba0ea..791400c475 100644 --- a/Sources/ApolloTestSupport/MockOperation.swift +++ b/Sources/ApolloTestSupport/MockOperation.swift @@ -12,7 +12,7 @@ open class MockOperation: GraphQLOperation { public var stubbedQueryDocument: String? public final var queryDocument: String { stubbedQueryDocument ?? operationDefinition } - open var variables: [String: InputValue]? + open var variables: Variables? public init(type: GraphQLOperationType = .query) { self.operationType = type diff --git a/Sources/ApolloWebSocket/OperationMessage.swift b/Sources/ApolloWebSocket/OperationMessage.swift index d0719262b8..2ade22bc8e 100644 --- a/Sources/ApolloWebSocket/OperationMessage.swift +++ b/Sources/ApolloWebSocket/OperationMessage.swift @@ -1,5 +1,6 @@ #if !COCOAPODS import Apollo +import ApolloAPI #endif import Foundation @@ -20,7 +21,7 @@ final class OperationMessage { } let serializationFormat = JSONSerializationFormat.self - let message: GraphQLMap + let message: JSONEncodableDictionary var serialized: String? var rawMessage : String? { @@ -32,10 +33,10 @@ final class OperationMessage { } } - init(payload: GraphQLMap? = nil, + init(payload: JSONEncodableDictionary? = nil, id: String? = nil, type: Types = .start) { - var message: GraphQLMap = [:] + var message: JSONEncodableDictionary = [:] if let payload = payload { message["payload"] = payload } diff --git a/Sources/ApolloWebSocket/WebSocketTransport.swift b/Sources/ApolloWebSocket/WebSocketTransport.swift index 145d2db797..2fdd384766 100644 --- a/Sources/ApolloWebSocket/WebSocketTransport.swift +++ b/Sources/ApolloWebSocket/WebSocketTransport.swift @@ -97,7 +97,7 @@ public class WebSocketTransport { /// Defaults to true. public let connectOnInit: Bool /// [optional]The payload to send on connection. Defaults to an empty `GraphQLMap`. - public fileprivate(set) var connectingPayload: GraphQLMap? + public fileprivate(set) var connectingPayload: JSONEncodableDictionary? /// The `RequestBodyCreator` to use when serializing requests. Defaults to an `ApolloRequestBodyCreator`. public let requestBodyCreator: RequestBodyCreator /// The `OperationMessageIdCreator` used to generate a unique message identifier per request. @@ -113,7 +113,7 @@ public class WebSocketTransport { reconnectionInterval: TimeInterval = 0.5, allowSendingDuplicates: Bool = true, connectOnInit: Bool = true, - connectingPayload: GraphQLMap? = [:], + connectingPayload: JSONEncodableDictionary? = [:], requestBodyCreator: RequestBodyCreator = ApolloRequestBodyCreator(), operationMessageIdCreator: OperationMessageIdCreator = ApolloSequencedOperationMessageIdCreator() ) { @@ -341,7 +341,7 @@ public class WebSocketTransport { } } - public func updateConnectingPayload(_ payload: GraphQLMap, reconnectIfConnected: Bool = true) { + public func updateConnectingPayload(_ payload: JSONEncodableDictionary, reconnectIfConnected: Bool = true) { self.config.connectingPayload = payload if reconnectIfConnected && isConnected() { diff --git a/Sources/UploadAPI/API.swift b/Sources/UploadAPI/API.swift index 4b133e5ea8..105031ad22 100644 --- a/Sources/UploadAPI/API.swift +++ b/Sources/UploadAPI/API.swift @@ -60,8 +60,8 @@ public final class UploadMultipleFilesToTheSameParameterMutation: GraphQLMutatio self.files = files } - public var variables: [String: InputValue]? { - ["files": files].toInputVariables() + public var variables: Variables? { + ["files": files] } public struct Data: SelectionSet { @@ -126,8 +126,8 @@ public final class UploadMultipleFilesToDifferentParametersMutation: GraphQLMuta self.multipleFiles = multipleFiles } - public var variables: [String: InputValue]? { - ["singleFile": singleFile, "multipleFiles": multipleFiles].toInputVariables() + public var variables: Variables? { + ["singleFile": singleFile, "multipleFiles": multipleFiles] } public struct Data: SelectionSet { @@ -200,8 +200,8 @@ public final class UploadOneFileMutation: GraphQLMutation { self.file = file } - public var variables: [String: InputValue]? { - ["file": file].toInputVariables() + public var variables: Variables? { + ["file": file] } public struct Data: SelectionSet { diff --git a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift index b2f1fb5839..746dd40f64 100644 --- a/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift +++ b/Tests/ApolloTests/AutomaticPersistedQueriesTests.swift @@ -1,4 +1,5 @@ import XCTest +import Nimble @testable import Apollo import ApolloAPI import ApolloTestSupport @@ -25,23 +26,23 @@ class AutomaticPersistedQueriesTests: XCTestCase { } } - fileprivate enum MockEnum: String, CaseIterable, InputValueConvertible { + fileprivate enum MockEnum: String, EnumType { case NEWHOPE case JEDI case EMPIRE } fileprivate class MockHeroNameQuery: MockQuery { - var episode: MockEnum? { + var episode: GraphQLNullable { didSet { - self.variables = ["episode": episode].toInputVariables() + self.variables = ["episode": episode] } } - init(episode: MockEnum? = nil) { + init(episode: GraphQLNullable = .none) { self.episode = episode super.init() - self.variables = ["episode": episode].toInputVariables() + self.variables = ["episode": episode] self.operationIdentifier = "f6e76545cd03aa21368d9969cb39447f6e836a16717823281803778e7805d671" self.operationDefinition = "MockHeroNameQuery - Operation Definition" } @@ -84,7 +85,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { if let query = operation as? MockHeroNameQuery{ if let variables = jsonBody["variables"] as? JSONObject { XCTAssertEqual(variables["episode"] as? String, - query.episode?.rawValue, + query.episode.rawValue, file: file, line: line) } else { @@ -156,16 +157,17 @@ class AutomaticPersistedQueriesTests: XCTestCase { } if let variables = url.queryItemDictionary?["variables"] { - if let episode = query.episode { - XCTAssertEqual(variables, - "{\"episode\":\"\(episode.rawValue)\"}", - file: file, - line: line) - } else { - XCTAssertEqual(variables, - "{\"episode\":null}", - file: file, - line: line) + let expectation = expect(file: file, line: line, variables) + switch query.episode { + case let .some(episode): + expectation.to(equal("{\"episode\":\"\(episode.rawValue)\"}")) + + case .none: + #warning("TODO: write test to test this case actually happens") + expectation.to(equal("{}")) + + case .null: + expectation.to(equal("{\"episode\":null}")) } } else { XCTFail("variables should not be nil", @@ -252,7 +254,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { endpointURL: Self.endpoint) let expectation = self.expectation(description: "Query sent") - let query = MockHeroNameQuery(episode: .JEDI) + let query = MockHeroNameQuery(episode: .some(.JEDI)) var lastRequest: URLRequest? let _ = network.send(operation: query) { _ in lastRequest = mockClient.lastRequest.value @@ -279,7 +281,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { autoPersistQueries: true) let expectation = self.expectation(description: "Query sent") - let query = MockHeroNameQuery(episode: .EMPIRE) + let query = MockHeroNameQuery(episode: .some(.EMPIRE)) var lastRequest: URLRequest? let _ = network.send(operation: query) { _ in lastRequest = mockClient.lastRequest.value @@ -360,7 +362,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { useGETForPersistedQueryRetry: true) let expectation = self.expectation(description: "Query sent") - let query = MockHeroNameQuery(episode: .EMPIRE) + let query = MockHeroNameQuery(episode: .some(.EMPIRE)) var lastRequest: URLRequest? let _ = network.send(operation: query) { _ in lastRequest = mockClient.lastRequest.value @@ -442,7 +444,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { autoPersistQueries: true) let expectation = self.expectation(description: "Query sent") - let query = MockHeroNameQuery(episode: .EMPIRE) + let query = MockHeroNameQuery(episode: .some(.EMPIRE)) var lastRequest: URLRequest? let _ = network.send(operation: query) { _ in lastRequest = mockClient.lastRequest.value @@ -470,7 +472,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { useGETForQueries: true) let expectation = self.expectation(description: "Query sent") - let query = MockHeroNameQuery(episode: .EMPIRE) + let query = MockHeroNameQuery(episode: .some(.EMPIRE)) var lastRequest: URLRequest? let _ = network.send(operation: query) { _ in lastRequest = mockClient.lastRequest.value @@ -498,7 +500,7 @@ class AutomaticPersistedQueriesTests: XCTestCase { useGETForPersistedQueryRetry: true) let expectation = self.expectation(description: "Query sent") - let query = MockHeroNameQuery(episode: .EMPIRE) + let query = MockHeroNameQuery(episode: .some(.EMPIRE)) var lastRequest: URLRequest? let _ = network.send(operation: query) { _ in lastRequest = mockClient.lastRequest.value diff --git a/Tests/ApolloTests/Cache/WatchQueryTests.swift b/Tests/ApolloTests/Cache/WatchQueryTests.swift index ed55ad73de..ae692a186e 100644 --- a/Tests/ApolloTests/Cache/WatchQueryTests.swift +++ b/Tests/ApolloTests/Cache/WatchQueryTests.swift @@ -1,4 +1,5 @@ import XCTest +import Nimble @testable import Apollo import ApolloAPI import ApolloTestSupport @@ -263,7 +264,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { runActivity("Fetch same query from server with different argument") { _ in let serverRequestExpectation = server.expect(MockQuery.self) { request in - XCTAssertEqual(request.operation.variables?["episode"], .scalar("JEDI")) + expect(request.operation.variables?["episode"] as? String).to(equal("JEDI")) return [ "data": [ @@ -323,7 +324,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { runActivity("Initial fetch from server") { _ in let serverRequestExpectation = server.expect(MockQuery.self) { request in - XCTAssertEqual(request.operation.variables?["episode"], "EMPIRE") + expect(request.operation.variables?["episode"] as? String).to(equal("EMPIRE")) return [ "data": [ "hero": [ @@ -353,7 +354,7 @@ class WatchQueryTests: XCTestCase, CacheDependentTesting { runActivity("Fetch same query from server with different argument but returning same object with changed data") { _ in let serverRequestExpectation = server.expect(MockQuery.self) { request in - XCTAssertEqual(request.operation.variables?["episode"], "JEDI") + expect(request.operation.variables?["episode"] as? String).to(equal("JEDI")) return [ "data": [ "hero": [ diff --git a/Tests/ApolloTests/CacheKeyForFieldTests.swift b/Tests/ApolloTests/CacheKeyForFieldTests.swift index b16d3de760..ecb62f6d1b 100644 --- a/Tests/ApolloTests/CacheKeyForFieldTests.swift +++ b/Tests/ApolloTests/CacheKeyForFieldTests.swift @@ -41,8 +41,20 @@ class CacheKeyForFieldTests: XCTestCase { XCTAssertEqual(field.test_cacheKey, "hero(episode:1.99)") } - func testFieldWithNilArgument() { - let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["episode": nil]) + func testFieldWithNullArgument() { + let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["episode": .null]) + XCTAssertEqual(field.test_cacheKey, "hero(episode:null)") + } + + func testFieldWithNestedNullArgument() throws { + let field = Selection.Field("hero", + type: .scalar(String.self), + arguments: ["nested": ["foo": 1, "bar": InputValue.null]]) + XCTAssertEqual(field.test_cacheKey, "hero([nested:bar:null,foo:1])") + } + + func testFieldWithArgumentOmitted() { + let field = Selection.Field("hero", type: .scalar(String.self), arguments: [:]) XCTAssertEqual(field.test_cacheKey, "hero") } @@ -58,7 +70,7 @@ class CacheKeyForFieldTests: XCTestCase { func testFieldWithDictionaryArgumentWithVariables() throws { let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["nested": ["foo": InputValue.variable("a"), "bar": InputValue.variable("b")]]) - let variables: [String: InputValue] = ["a": 1, "b": 2] + let variables = ["a": 1, "b": 2] XCTAssertEqual(try field.cacheKey(with: variables), "hero([nested:bar:2,foo:1])") } @@ -76,13 +88,25 @@ class CacheKeyForFieldTests: XCTestCase { func testFieldWithVariableArgument() throws { let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["episode": .variable("episode")]) - let variables: [String: InputValue] = ["episode": "JEDI"] + let variables = ["episode": "JEDI"] XCTAssertEqual(try field.cacheKey(with: variables), "hero(episode:JEDI)") } func testFieldWithVariableArgumentWithNil() throws { let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["episode": .variable("episode")]) - let variables: [String: InputValue] = ["episode": nil] + let variables: GraphQLOperation.Variables = ["episode": GraphQLNullable.none] XCTAssertEqual(try field.cacheKey(with: variables), "hero") } + + func testFieldWithVariableArgumentWithNull() throws { + let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["episode": .variable("episode")]) + let variables = ["episode": GraphQLNullable.null] + XCTAssertEqual(try field.cacheKey(with: variables), "hero(episode:null)") + } + + func testFieldWithVariableArgumentWithNestedNull() throws { + let field = Selection.Field("hero", type: .scalar(String.self), arguments: ["nested": ["foo": InputValue.variable("a"), "bar": InputValue.variable("b")]]) + let variables: GraphQLOperation.Variables = ["a": 1, "b": GraphQLNullable.null] + XCTAssertEqual(try field.cacheKey(with: variables), "hero([nested:bar:null,foo:1])") + } } diff --git a/Tests/ApolloTests/GETTransformerTests.swift b/Tests/ApolloTests/GETTransformerTests.swift index ff56ece31b..be5adc62f0 100644 --- a/Tests/ApolloTests/GETTransformerTests.swift +++ b/Tests/ApolloTests/GETTransformerTests.swift @@ -20,7 +20,7 @@ class GETTransformerTests: XCTestCase { super.tearDown() } - private enum MockEnum: String, CaseIterable, InputValueConvertible { + private enum MockEnum: String, EnumType { case LARGE case AVERAGE case SMALL @@ -64,7 +64,7 @@ query MockQuery($param: MockEnum) { } } """ - operation.variables = ["param": MockEnum.LARGE.asInputValue] + operation.variables = ["param": MockEnum.LARGE] let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false, @@ -113,18 +113,17 @@ query MockQuery($a: String, $b: Boolean!) { operation.stubbedQueryDocument = "query MockQuery {}" operation.operationIdentifier = "4d465fbc6e3731d01102504850" - let persistedQuery: GraphQLMap = [ + let persistedQuery: JSONEncodableDictionary = [ "version": 1, - "sha256Hash": operation.operationIdentifier + "sha256Hash": operation.operationIdentifier! ] - let extensions: GraphQLMap = [ + let extensions: JSONEncodableDictionary = [ "persistedQuery": persistedQuery ] - let body: GraphQLMap = [ + let body: JSONEncodableDictionary = [ "query": operation.queryDocument, - "variables": operation.variables, "extensions": extensions ] @@ -140,13 +139,12 @@ query MockQuery($a: String, $b: Boolean!) { func test__createGetURL__queryWithParameter_withPlusSign_encodesPlusSign() throws { let operation = MockOperation.mock() - let extensions: GraphQLMap = [ + let extensions: JSONEncodableDictionary = [ "testParam": "+Test+Test" ] - let body: GraphQLMap = [ + let body: JSONEncodableDictionary = [ "query": operation.queryDocument, - "variables": operation.variables, "extensions": extensions ] @@ -162,13 +160,12 @@ query MockQuery($a: String, $b: Boolean!) { func test__createGetURL__queryWithParameter_withAmpersand_encodesAmpersand() throws { let operation = MockOperation.mock() - let extensions: GraphQLMap = [ + let extensions: JSONEncodableDictionary = [ "testParam": "Test&Test" ] - let body: GraphQLMap = [ + let body: JSONEncodableDictionary = [ "query": operation.queryDocument, - "variables": operation.variables, "extensions": extensions ] @@ -185,17 +182,16 @@ query MockQuery($a: String, $b: Boolean!) { operation.operationName = "TestOpName" operation.operationIdentifier = "4d465fbc6e3731d01102504850" - let persistedQuery: GraphQLMap = [ + let persistedQuery: JSONEncodableDictionary = [ "version": 1, - "sha256Hash": operation.operationIdentifier + "sha256Hash": operation.operationIdentifier! ] - let extensions: GraphQLMap = [ + let extensions: JSONEncodableDictionary = [ "persistedQuery": persistedQuery ] - let body: GraphQLMap = [ - "variables": operation.variables, + let body: JSONEncodableDictionary = [ "extensions": extensions ] @@ -219,7 +215,7 @@ query MockQuery($param: String) { } } """ - operation.variables = ["param": .none] + operation.variables = ["param": GraphQLNullable.null] let body = requestBodyCreator.requestBody(for: operation, sendOperationIdentifiers: false, @@ -238,13 +234,12 @@ query MockQuery($param: String) { func test__createGetURL__urlHasExistingParameters_encodesURLIncludingExistingParameters_atStartOfQueryParameters() throws { let operation = MockOperation.mock() - let extensions: GraphQLMap = [ + let extensions: JSONEncodableDictionary = [ "testParam": "Test&Test" ] - let body: GraphQLMap = [ + let body: JSONEncodableDictionary = [ "query": operation.queryDocument, - "variables": operation.variables, "extensions": extensions ] @@ -260,7 +255,7 @@ query MockQuery($param: String) { } func test__createGetURL__withEmptyQueryParameter_returnsURL() throws { - let body: GraphQLMap = [:] + let body: JSONEncodableDictionary = [:] let transformer = GraphQLGETTransformer(body: body, url: Self.url) let url = transformer.createGetURL() diff --git a/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift index 403e4fb8ad..39d29c2722 100644 --- a/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_ResultNormalizer_FromResponse_Tests.swift @@ -19,7 +19,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { private func normalizeRecords( _ selectionSet: RootSelectionSet.Type, - with variables: [String: InputValue]? = nil, + with variables: GraphQLOperation.Variables? = nil, from object: JSONObject ) throws -> RecordSet { return try GraphQLExecutor_ResultNormalizer_FromResponse_Tests.executor.execute( @@ -76,7 +76,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { } } - let variables: [String: InputValue] = ["episode": "JEDI"] + let variables = ["episode": "JEDI"] let object: JSONObject = [ "hero": ["__typename": "Droid", "name": "R2-D2"] @@ -95,7 +95,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { func test__execute__givenObjectWithNoCacheKey_forFieldWithEnumArgument_normalizesRecordToPathFromQueryRootIncludingArgument() throws { // given - enum MockEnum: String, CaseIterable, InputValueConvertible { + enum MockEnum: String, EnumType { case NEWHOPE case EMPIRE case JEDI @@ -113,7 +113,7 @@ class GraphQLExecutor_ResultNormalizer_FromResponse_Tests: XCTestCase { } } - let variables: [String: InputValue] = ["episode": MockEnum.EMPIRE].toInputVariables() + let variables = ["episode": MockEnum.EMPIRE] let object: JSONObject = [ "hero": ["__typename": "Droid", "name": "R2-D2"] diff --git a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift index 444f2303bc..1bfbe799d3 100644 --- a/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift +++ b/Tests/ApolloTests/GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.swift @@ -20,7 +20,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { private func readValues( _ selectionSet: T.Type, from object: JSONObject, - variables: [String: InputValue]? = nil + variables: GraphQLOperation.Variables? = nil ) throws -> T { return try GraphQLExecutor_SelectionSetMapper_FromResponse_Tests.executor.execute( selectionSet: selectionSet, @@ -205,7 +205,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { // MARK: Nonnull Enum Value - private enum MockEnum: String, CaseIterable, InputValueConvertible { + private enum MockEnum: String, EnumType { case SMALL case MEDIUM case LARGE @@ -814,7 +814,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -831,7 +831,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -849,7 +849,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -870,7 +870,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -891,7 +891,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -916,7 +916,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -941,7 +941,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -972,7 +972,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["__typename": "Person", "name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1003,7 +1003,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["__typename": "Person", "name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1034,7 +1034,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["__typename": "Person", "name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1065,7 +1065,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["__typename": "Person", "name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1096,7 +1096,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["__typename": "Person", "name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1132,7 +1132,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { let object: JSONObject = ["__typename": "Person", "name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1151,7 +1151,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1168,7 +1168,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1188,7 +1188,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": false].toInputVariables() + let variables = ["variable": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1209,7 +1209,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1234,7 +1234,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { ]} } let object: JSONObject = ["name": "Luke Skywalker"] - let variables = ["variable": true].toInputVariables() + let variables = ["variable": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1259,7 +1259,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": true, - "include": true].toInputVariables() + "include": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1282,7 +1282,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": true, - "include": false].toInputVariables() + "include": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1305,7 +1305,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": false, - "include": false].toInputVariables() + "include": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1328,7 +1328,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": false, - "include": true].toInputVariables() + "include": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1348,7 +1348,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": true, - "include": true].toInputVariables() + "include": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1367,7 +1367,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": false, - "include": false].toInputVariables() + "include": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1386,7 +1386,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": false, - "include": true].toInputVariables() + "include": true] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) @@ -1405,7 +1405,7 @@ class GraphQLExecutor_SelectionSetMapper_FromResponse_Tests: XCTestCase { } let object: JSONObject = ["name": "Luke Skywalker", "id": "1234"] let variables = ["skip": true, - "include": false].toInputVariables() + "include": false] // when let data = try readValues(GivenSelectionSet.self, from: object, variables: variables) diff --git a/Tests/ApolloTests/GraphQLMapEncodingTests.swift b/Tests/ApolloTests/GraphQLMapEncodingTests.swift index 0f52ba39a8..acf896d773 100644 --- a/Tests/ApolloTests/GraphQLMapEncodingTests.swift +++ b/Tests/ApolloTests/GraphQLMapEncodingTests.swift @@ -6,9 +6,9 @@ import StarWarsAPI #warning("TODO: Do we refactor these for [String: InputValue] or delete them?") class GraphQLMapEncodingTests: XCTestCase { - private struct MockGraphQLMapConvertible: GraphQLMapConvertible { - let graphQLMap: GraphQLMap - } +// private struct MockGraphQLMapConvertible: GraphQLMapConvertible { +// let graphQLMap: GraphQLMap +// } // private func serializeAndDeserialize(_ map: GraphQLMap) -> NSDictionary { // let input = MockGraphQLMapConvertible(graphQLMap: map) diff --git a/Tests/ApolloTests/JSONValueMatcher.swift b/Tests/ApolloTests/JSONValueMatcher.swift index 10e0b121d6..047f075a98 100644 --- a/Tests/ApolloTests/JSONValueMatcher.swift +++ b/Tests/ApolloTests/JSONValueMatcher.swift @@ -36,7 +36,7 @@ public func equalJSONValue(_ expectedValue: JSONEncodable?) -> Predicate Predicate { +public func equal(_ expectedValue: JSONEncodableDictionary?) -> Predicate { return Predicate { actual in let msg = ExpectationMessage.expectedActualValueTo("equal <\(stringify(expectedValue))>") if let actualValue = try actual.evaluate(), let expectedValue = expectedValue { diff --git a/Tests/ApolloTests/RequestBodyCreatorTests.swift b/Tests/ApolloTests/RequestBodyCreatorTests.swift index d0c92b87cd..e01d6cf3d5 100644 --- a/Tests/ApolloTests/RequestBodyCreatorTests.swift +++ b/Tests/ApolloTests/RequestBodyCreatorTests.swift @@ -14,7 +14,10 @@ import ApolloTestSupport class RequestBodyCreatorTests: XCTestCase { - func create(with creator: RequestBodyCreator, for operation: Operation) -> GraphQLMap { + func create( + with creator: RequestBodyCreator, + for operation: Operation + ) -> JSONEncodableDictionary { creator.requestBody(for: operation, sendOperationIdentifiers: false, sendQueryDocument: true, @@ -53,6 +56,10 @@ class RequestBodyCreatorTests: XCTestCase { expect(actual).to(equalJSONValue(expected)) } - #warning("TODO: Test generated input objects converted to variables correctly.") + #warning(""" +TODO: Test generated input objects converted to variables correctly. +- nil variable value +- null variable value +""") } diff --git a/Tests/ApolloTests/TestCustomRequestBodyCreator.swift b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift index 4a11d3824b..48e349f5b2 100644 --- a/Tests/ApolloTests/TestCustomRequestBodyCreator.swift +++ b/Tests/ApolloTests/TestCustomRequestBodyCreator.swift @@ -11,13 +11,13 @@ import ApolloAPI struct TestCustomRequestBodyCreator: RequestBodyCreator { - var stubbedRequestBody: GraphQLMap = ["TestCustomRequestBodyCreator": "TestBodyValue"] + var stubbedRequestBody: JSONEncodableDictionary = ["TestCustomRequestBodyCreator": "TestBodyValue"] func requestBody( for operation: Operation, sendOperationIdentifiers: Bool, sendQueryDocument: Bool, autoPersistQuery: Bool - ) -> GraphQLMap { + ) -> JSONEncodableDictionary { stubbedRequestBody } } diff --git a/Tests/ApolloTests/WebSocket/WebSocketTests.swift b/Tests/ApolloTests/WebSocket/WebSocketTests.swift index 6688f91988..714a7f5321 100644 --- a/Tests/ApolloTests/WebSocket/WebSocketTests.swift +++ b/Tests/ApolloTests/WebSocket/WebSocketTests.swift @@ -6,7 +6,7 @@ import ApolloTestSupport @testable import ApolloWebSocket extension WebSocketTransport { - func write(message: GraphQLMap) { + func write(message: JSONEncodableDictionary) { let serialized = try! JSONSerializationFormat.serialize(value: message) if let str = String(data: serialized, encoding: .utf8) { self.websocket.write(string: str) @@ -72,7 +72,7 @@ class WebSocketTests: XCTestCase { } } - let message : GraphQLMap = [ + let message : JSONEncodableDictionary = [ "type": "data", "id": "1", "payload": [ @@ -127,7 +127,7 @@ class WebSocketTests: XCTestCase { } } - let message : GraphQLMap = [ + let message : JSONEncodableDictionary = [ "type": "data", "id": "2", // subscribing on id = 1, i.e. expecting error when receiving id = 2 "payload": [ @@ -170,7 +170,7 @@ class WebSocketTests: XCTestCase { } } - let message : GraphQLMap = [ + let message : JSONEncodableDictionary = [ "type": "data", "id": "12345678", // subscribing on id = 12345678 from custom operation id "payload": [