From e7daf861af29adbb2a214b8d5090da179ed0c9c0 Mon Sep 17 00:00:00 2001 From: Anthony Miller Date: Tue, 7 Sep 2021 13:53:52 -0700 Subject: [PATCH] Move common objects into ApolloAPI --- Apollo.xcodeproj/project.pbxproj | 44 ++++- .../CodegenV1/CacheTransaction.swift | 106 +++++++++++ Sources/ApolloAPI/CodegenV1/Cacheable.swift | 12 ++ Sources/ApolloAPI/CodegenV1/GraphQLEnum.swift | 35 +++- .../ApolloAPI/CodegenV1/GraphQLSchema.swift | 32 ---- .../ApolloAPI/CodegenV1/ResponseDict.swift | 22 +-- .../CodegenV1/SchemaTypeFactory.swift | 3 + .../CodegenV1/SchemaTypes/Field.swift | 103 ++++++++++ .../CodegenV1/SchemaTypes/Interface.swift | 53 ++++++ .../CodegenV1/SchemaTypes/Object.swift | 143 ++++++++++++++ .../CodegenV1/SchemaTypes/ObjectType.swift | 6 + .../CodegenV1/SchemaTypes/Union.swift | 177 ++++++++++++++++++ .../ApolloAPI/CodegenV1/SelectionSet.swift | 34 ++-- Sources/ApolloAPI/ScalarTypes.swift | 21 ++- 14 files changed, 714 insertions(+), 77 deletions(-) create mode 100644 Sources/ApolloAPI/CodegenV1/CacheTransaction.swift create mode 100644 Sources/ApolloAPI/CodegenV1/Cacheable.swift delete mode 100644 Sources/ApolloAPI/CodegenV1/GraphQLSchema.swift create mode 100644 Sources/ApolloAPI/CodegenV1/SchemaTypeFactory.swift create mode 100644 Sources/ApolloAPI/CodegenV1/SchemaTypes/Field.swift create mode 100644 Sources/ApolloAPI/CodegenV1/SchemaTypes/Interface.swift create mode 100644 Sources/ApolloAPI/CodegenV1/SchemaTypes/Object.swift create mode 100644 Sources/ApolloAPI/CodegenV1/SchemaTypes/ObjectType.swift create mode 100644 Sources/ApolloAPI/CodegenV1/SchemaTypes/Union.swift diff --git a/Apollo.xcodeproj/project.pbxproj b/Apollo.xcodeproj/project.pbxproj index ebea436e6e..3a94e37d3d 100644 --- a/Apollo.xcodeproj/project.pbxproj +++ b/Apollo.xcodeproj/project.pbxproj @@ -186,7 +186,6 @@ DE05860D266978A100265760 /* ScalarTypes.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B15260A6FCA00D2F4FF /* ScalarTypes.swift */; }; DE05860E266978A100265760 /* GraphQLOptional.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */; }; DE058610266978A100265760 /* InputValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9C11E2D3CAF0023C4D5 /* InputValue.swift */; }; - DE058613266978A100265760 /* GraphQLSchema.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B13260A6FCA00D2F4FF /* GraphQLSchema.swift */; }; DE058616266978A100265760 /* GraphQLEnum.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE3C7B14260A6FCA00D2F4FF /* GraphQLEnum.swift */; }; DE05862D2669800000265760 /* Matchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9BE071AE2368D34D00FA5952 /* Matchable.swift */; }; DE05862F266980C200265760 /* GraphQLSelectionSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FC9A9C41E2D6CE70023C4D5 /* GraphQLSelectionSet.swift */; }; @@ -202,6 +201,14 @@ DE181A3226C5C401000C0B9C /* Compression.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3126C5C401000C0B9C /* Compression.swift */; }; DE181A3426C5D8D4000C0B9C /* CompressionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */; }; DE181A3626C5DE4F000C0B9C /* WebSocketStream.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */; }; + DE2FCF1D26E806710057EA67 /* SchemaTypeFactory.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1C26E806710057EA67 /* SchemaTypeFactory.swift */; }; + DE2FCF1F26E807CC0057EA67 /* CacheTransaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */; }; + DE2FCF2126E807EF0057EA67 /* Cacheable.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2026E807EF0057EA67 /* Cacheable.swift */; }; + DE2FCF2726E8083A0057EA67 /* Union.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2326E8083A0057EA67 /* Union.swift */; }; + DE2FCF2826E8083A0057EA67 /* Interface.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2426E8083A0057EA67 /* Interface.swift */; }; + DE2FCF2926E8083A0057EA67 /* Object.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2526E8083A0057EA67 /* Object.swift */; }; + DE2FCF2A26E8083A0057EA67 /* Field.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2626E8083A0057EA67 /* Field.swift */; }; + DE2FCF2C26E808560057EA67 /* ObjectType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE2FCF2B26E808560057EA67 /* ObjectType.swift */; }; DE3C7974260A646300D2F4FF /* dist in Resources */ = {isa = PBXBuildFile; fileRef = DE3C7973260A646300D2F4FF /* dist */; }; DE3C7A94260A6C1000D2F4FF /* ApolloUtils.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9B68353E2463481A00337AE6 /* ApolloUtils.framework */; }; DE3C7A95260A6C1000D2F4FF /* ApolloUtils.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 9B68353E2463481A00337AE6 /* ApolloUtils.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -791,6 +798,14 @@ DE181A3126C5C401000C0B9C /* Compression.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Compression.swift; sourceTree = ""; }; DE181A3326C5D8D4000C0B9C /* CompressionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompressionTests.swift; sourceTree = ""; }; DE181A3526C5DE4F000C0B9C /* WebSocketStream.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketStream.swift; sourceTree = ""; }; + DE2FCF1C26E806710057EA67 /* SchemaTypeFactory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SchemaTypeFactory.swift; sourceTree = ""; }; + DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CacheTransaction.swift; sourceTree = ""; }; + DE2FCF2026E807EF0057EA67 /* Cacheable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Cacheable.swift; sourceTree = ""; }; + DE2FCF2326E8083A0057EA67 /* Union.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Union.swift; sourceTree = ""; }; + DE2FCF2426E8083A0057EA67 /* Interface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Interface.swift; sourceTree = ""; }; + DE2FCF2526E8083A0057EA67 /* Object.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Object.swift; sourceTree = ""; }; + DE2FCF2626E8083A0057EA67 /* Field.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Field.swift; sourceTree = ""; }; + DE2FCF2B26E808560057EA67 /* ObjectType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ObjectType.swift; sourceTree = ""; }; DE3C7973260A646300D2F4FF /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; DE3C79A9260A6ACD00D2F4FF /* AnimalKingdomAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = AnimalKingdomAPI.h; sourceTree = ""; }; DE3C79AA260A6ACD00D2F4FF /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -804,7 +819,6 @@ DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SelectionSet.swift; sourceTree = ""; }; DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResponseDict.swift; sourceTree = ""; }; DE3C7B12260A6FC900D2F4FF /* FragmentProtocols.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FragmentProtocols.swift; sourceTree = ""; }; - DE3C7B13260A6FCA00D2F4FF /* GraphQLSchema.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLSchema.swift; sourceTree = ""; }; DE3C7B14260A6FCA00D2F4FF /* GraphQLEnum.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GraphQLEnum.swift; sourceTree = ""; }; DE3C7B15260A6FCA00D2F4FF /* ScalarTypes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ScalarTypes.swift; sourceTree = ""; }; DE56DC222683B2020090D6E4 /* DefaultInterceptorProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultInterceptorProvider.swift; sourceTree = ""; }; @@ -1682,6 +1696,18 @@ path = Sources/ApolloAPI; sourceTree = ""; }; + DE2FCF2226E8082A0057EA67 /* SchemaTypes */ = { + isa = PBXGroup; + children = ( + DE2FCF2626E8083A0057EA67 /* Field.swift */, + DE2FCF2426E8083A0057EA67 /* Interface.swift */, + DE2FCF2526E8083A0057EA67 /* Object.swift */, + DE2FCF2326E8083A0057EA67 /* Union.swift */, + DE2FCF2B26E808560057EA67 /* ObjectType.swift */, + ); + path = SchemaTypes; + sourceTree = ""; + }; DE3C79A8260A6ACD00D2F4FF /* AnimalKingdomAPI */ = { isa = PBXGroup; children = ( @@ -1709,13 +1735,16 @@ DE3C7B0F260A6F7F00D2F4FF /* CodegenV1 */ = { isa = PBXGroup; children = ( + DE2FCF2226E8082A0057EA67 /* SchemaTypes */, + DE2FCF1E26E807CC0057EA67 /* CacheTransaction.swift */, 9B68F06E241C649E00E97318 /* GraphQLOptional.swift */, DE3C7B12260A6FC900D2F4FF /* FragmentProtocols.swift */, DE3C7B14260A6FCA00D2F4FF /* GraphQLEnum.swift */, - DE3C7B13260A6FCA00D2F4FF /* GraphQLSchema.swift */, DE3C7B11260A6FC900D2F4FF /* ResponseDict.swift */, DE3C7B10260A6FC900D2F4FF /* SelectionSet.swift */, + DE2FCF1C26E806710057EA67 /* SchemaTypeFactory.swift */, DE664ED326602AF60054DB4F /* Selection.swift */, + DE2FCF2026E807EF0057EA67 /* Cacheable.swift */, ); path = CodegenV1; sourceTree = ""; @@ -2836,12 +2865,19 @@ DE058609266978A100265760 /* Selection.swift in Sources */, DE05860A266978A100265760 /* ResponseDict.swift in Sources */, DE05860B266978A100265760 /* SelectionSet.swift in Sources */, + DE2FCF1D26E806710057EA67 /* SchemaTypeFactory.swift in Sources */, + DE2FCF2C26E808560057EA67 /* ObjectType.swift in Sources */, DE05860C266978A100265760 /* FragmentProtocols.swift in Sources */, + DE2FCF2A26E8083A0057EA67 /* Field.swift in Sources */, DE05860D266978A100265760 /* ScalarTypes.swift in Sources */, DE05860E266978A100265760 /* GraphQLOptional.swift in Sources */, + DE2FCF2126E807EF0057EA67 /* Cacheable.swift in Sources */, + DE2FCF1F26E807CC0057EA67 /* CacheTransaction.swift in Sources */, + DE2FCF2826E8083A0057EA67 /* Interface.swift in Sources */, + DE2FCF2926E8083A0057EA67 /* Object.swift in Sources */, DE058610266978A100265760 /* InputValue.swift in Sources */, - DE058613266978A100265760 /* GraphQLSchema.swift in Sources */, DE058616266978A100265760 /* GraphQLEnum.swift in Sources */, + DE2FCF2726E8083A0057EA67 /* Union.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/Sources/ApolloAPI/CodegenV1/CacheTransaction.swift b/Sources/ApolloAPI/CodegenV1/CacheTransaction.swift new file mode 100644 index 0000000000..6e5154f5b7 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/CacheTransaction.swift @@ -0,0 +1,106 @@ +import Foundation + +public protocol CacheKeyResolver { + func cacheKey(for: [String: Any]) -> CacheKey? +} + +public class CacheTransaction { + let objectFactory: SchemaTypeFactory.Type + let keyResolver: CacheKeyResolver + private(set) var errors: [CacheError] = [] + private var fetchedObjects: [CacheKey: Object] = [:] + + init( + objectFactory: SchemaTypeFactory.Type, + keyResolver: CacheKeyResolver + ) { + self.objectFactory = objectFactory + self.keyResolver = keyResolver + } + + func object(withKey key: CacheKey) -> Object? { + fetchedObjects[key] // TODO: if not fetched yet, fetch from store + } + + func object(withData data: [String: Any]) -> Object { + let cacheKey = keyResolver.cacheKey(for: data) + + if let cacheKey = cacheKey, let object = fetchedObjects[cacheKey] { + return object + // TODO: should merge data objects if needed? + } + + guard let typename = data["__typename"] as? String, + let type = objectFactory.objectType(forTypename: typename) else { + fatalError() + } + + let object = type.init(transaction: self, data: data) + if let cacheKey = cacheKey { + fetchedObjects[cacheKey] = object + } + + return object + } + + func log(_ error: CacheError) { + errors.append(error) + } + + func log(_ error: Error) { + // TODO + } + +// func interface(withData data: [String: Any]) -> T { +// return T.init(object: object(withData: data)) +// } +} + +struct CacheError: Error, Equatable { + enum Reason: Error { + case unrecognizedCacheData(_ data: Any, forType: Any.Type) + case invalidObjectType(_ type: Object.Type, forExpectedType: Cacheable.Type) + case invalidValue(_ value: Cacheable?, forCovariantFieldOfType: ObjectType.Type) + case objectNotFound(forCacheKey: CacheKey) + } + + enum `Type` { + case read, write + } + + let reason: Reason + let type: Type + let field: String + let object: ObjectType? + + var message: String { + switch self.reason { + case let .unrecognizedCacheData(data, forType: type): + return "Cache data '\(data)' was unrecognized for conversion to type '\(type)'." + + case let .invalidObjectType(type, forExpectedType: expectedType): + switch expectedType { + case is Interface.Type: + return "Object of type '\(type)' does not implement interface '\(expectedType)'." + case is AnyUnion.Type: + return "Object of type '\(type)' is not a valid type for union '\(expectedType)'." + default: + return "Object of type '\(type)' is not a valid type for '\(expectedType)'." + } + + case let .invalidValue(value, forCovariantFieldOfType: fieldType): + return """ + Value '\(value ?? "nil")' is not a valid value for covariant field '\(field)'. + Object of type '\(Swift.type(of: object))' expects value of type '\(fieldType)'. + """ + + case let .objectNotFound(forCacheKey: key): + return "Object with cache key \(key.key) was not found in the cache." + + } + } + + static func ==(lhs: CacheError, rhs: CacheError) -> Bool { + lhs.message == rhs.message + } +} diff --git a/Sources/ApolloAPI/CodegenV1/Cacheable.swift b/Sources/ApolloAPI/CodegenV1/Cacheable.swift new file mode 100644 index 0000000000..be36318d16 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/Cacheable.swift @@ -0,0 +1,12 @@ +/// A type that can be the value of a `@CacheField` property. In other words, a `Cacheable` type +/// can be the value of a field on an `Object` or `Interface` +/// +/// # Conforming Types: +/// - `Object` +/// - `Interface` +/// - `ScalarType` (`String`, `Int`, `Bool`, `Float`) +/// - `CustomScalarType` +/// - `GraphQLEnum` (via `CustomScalarType`) +public protocol Cacheable { + static func value(with cacheData: Any, in transaction: CacheTransaction) throws -> Self +} diff --git a/Sources/ApolloAPI/CodegenV1/GraphQLEnum.swift b/Sources/ApolloAPI/CodegenV1/GraphQLEnum.swift index 692fc5a95e..ea3656e1c0 100644 --- a/Sources/ApolloAPI/CodegenV1/GraphQLEnum.swift +++ b/Sources/ApolloAPI/CodegenV1/GraphQLEnum.swift @@ -29,15 +29,15 @@ where T: RawRepresentable & CaseIterable, T.RawValue == String { /// The underlying enum case. If the value is `__unknown`, this will be `nil`. public var value: T? { switch self { - case .case(let value): return value + case let .case(value): return value default: return nil } } public var rawValue: String { switch self { - case .case(let value): return value.rawValue - case .__unknown(let value): return value + case let .case(value): return value.rawValue + case let .__unknown(value): return value } } @@ -48,7 +48,19 @@ where T: RawRepresentable & CaseIterable, T.RawValue == String { } } -/// Equatable +// MARK: CustomScalarType +extension GraphQLEnum: CustomScalarType { + public init(scalarData: Any) throws { + guard let stringData = scalarData as? String else { + throw CacheError.Reason.unrecognizedCacheData(scalarData, forType: Self.self) + } + self.init(rawValue: stringData) + } + + public var jsonValue: Any { rawValue } +} + +// MARK: Equatable extension GraphQLEnum { public static func ==(lhs: GraphQLEnum, rhs: GraphQLEnum) -> Bool { return lhs.rawValue == rhs.rawValue @@ -63,6 +75,8 @@ extension GraphQLEnum { } } +// MARK: Optional> Equatable + public func ==(lhs: GraphQLEnum?, rhs: T) -> Bool where T.RawValue == String { return lhs?.rawValue == rhs.rawValue @@ -73,10 +87,13 @@ where T.RawValue == String { return lhs?.rawValue != rhs.rawValue } -public func ~=(lhs: T, rhs: GraphQLEnum) -> Bool { - switch rhs { - case let .case(rhs) where rhs == lhs: return true - case let .__unknown(rhsRawValue) where rhsRawValue == lhs.rawValue: return true - default: return false +// MARK: Pattern Matching +extension GraphQLEnum { + public static func ~=(lhs: T, rhs: GraphQLEnum) -> Bool { + switch rhs { + case let .case(rhs) where rhs == lhs: return true + case let .__unknown(rhsRawValue) where rhsRawValue == lhs.rawValue: return true + default: return false + } } } diff --git a/Sources/ApolloAPI/CodegenV1/GraphQLSchema.swift b/Sources/ApolloAPI/CodegenV1/GraphQLSchema.swift deleted file mode 100644 index 34d73bd1f0..0000000000 --- a/Sources/ApolloAPI/CodegenV1/GraphQLSchema.swift +++ /dev/null @@ -1,32 +0,0 @@ -/// A protocol that a generated GraphQL Schema should conform to. -/// -/// A `GraphQLSchema` contains information on the types within a schema and their relationships -/// to other types. This information is used to verify that a `SelectionSet` can be converted to -/// a given type condition. -public protocol GraphQLSchema { - associatedtype ObjectType: SchemaObjectType where ObjectType.Interface == Self.Interface - associatedtype Union: SchemaUnion where Union.ObjectType == Self.ObjectType - associatedtype Interface -} - -public protocol SchemaTypeEnum: RawRepresentable, Equatable where RawValue == String {} - -public protocol SchemaObjectType: SchemaTypeEnum { - associatedtype Interface: SchemaTypeEnum - - static var unknownCase: Self { get } - - var implementedInterfaces: [Interface] { get } -} - -extension SchemaObjectType { - func implements(_ interface: Interface) -> Bool { - implementedInterfaces.contains(interface) - } -} - -public protocol SchemaUnion: SchemaTypeEnum { - associatedtype ObjectType - - var possibleTypes: [ObjectType] { get } -} diff --git a/Sources/ApolloAPI/CodegenV1/ResponseDict.swift b/Sources/ApolloAPI/CodegenV1/ResponseDict.swift index fd8d208754..9b8ba21463 100644 --- a/Sources/ApolloAPI/CodegenV1/ResponseDict.swift +++ b/Sources/ApolloAPI/CodegenV1/ResponseDict.swift @@ -7,32 +7,32 @@ public struct ResponseDict { data[key] as! T } - public subscript(_ key: String) -> T? { + public subscript(_ key: String) -> T? { data[key] as? T } public subscript(_ key: String) -> T { - let entityData = data[key] as! [String: Any] - return T.init(data: ResponseDict(data: entityData)) + let objectData = data[key] as! [String: Any] + return T.init(data: ResponseDict(data: objectData)) } public subscript(_ key: String) -> T? { - guard let entityData = data[key] as? [String: Any] else { return nil } - return T.init(data: ResponseDict(data: entityData)) + guard let objectData = data[key] as? [String: Any] else { return nil } + return T.init(data: ResponseDict(data: objectData)) } public subscript(_ key: String) -> [T] { - let entityData = data[key] as! [[String: Any]] - return entityData.map { T.init(data: ResponseDict(data: $0)) } + let objectData = data[key] as! [[String: Any]] + return objectData.map { T.init(data: ResponseDict(data: $0)) } } public subscript(_ key: String) -> GraphQLEnum { - let entityData = data[key] as! String - return GraphQLEnum(rawValue: entityData) + let objectData = data[key] as! String + return GraphQLEnum(rawValue: objectData) } public subscript(_ key: String) -> GraphQLEnum? { - guard let entityData = data[key] as? String else { return nil } - return GraphQLEnum(rawValue: entityData) + guard let objectData = data[key] as? String else { return nil } + return GraphQLEnum(rawValue: objectData) } } diff --git a/Sources/ApolloAPI/CodegenV1/SchemaTypeFactory.swift b/Sources/ApolloAPI/CodegenV1/SchemaTypeFactory.swift new file mode 100644 index 0000000000..4f6d9bfe4e --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/SchemaTypeFactory.swift @@ -0,0 +1,3 @@ +public protocol SchemaTypeFactory { + static func objectType(forTypename __typename: String) -> Object.Type? +} diff --git a/Sources/ApolloAPI/CodegenV1/SchemaTypes/Field.swift b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Field.swift new file mode 100644 index 0000000000..791760d0c8 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Field.swift @@ -0,0 +1,103 @@ +@propertyWrapper +public struct Field { + + let field: StaticString + + public init(_ field: StaticString) { + self.field = field + } + + public static subscript( + _enclosingInstance instance: E, + wrapped wrappedKeyPath: ReferenceWritableKeyPath, + storage storageKeyPath: ReferenceWritableKeyPath + ) -> T? { + get { + let wrapper = instance[keyPath: storageKeyPath] + let field = wrapper.field.description + guard let data = instance.data[field] else { + return nil + } + + do { + let value = try T.value(with: data, in: instance._transaction) + try wrapper.replace(data: data, with: value, on: instance) + return value + + } catch { + instance._transaction.log(error) + return nil + } + } + set { + let wrapper = instance[keyPath: storageKeyPath] + let field = wrapper.field.description + do { +// +// switch newValue { +// case .none: // TODO +// case is ScalarType: + try instance.set(value: newValue, forField: wrapper) +// case let object as Object: +// +// +// +// default: +// break // TODO +// } + } catch let error as CacheError.Reason { + let error = CacheError( + reason: error, + type: .write, + field: field, + object: object(for: instance) + ) + instance._transaction.log(error) + + } catch { + instance._transaction.log(error) + } + } + } + + private func replace( + data: Any, + with parsedValue: T, + on instance: ObjectType + ) throws { + /// Only need to do this for Object, Enums, and custom scalars. + /// DO NOT DO THIS when value is a CacheInterface ON a CacheInterface instance + /// For ScalarTypes, its redundant + /// TODO: Write tests for this. + switch (parsedValue) { + case is Object where !(data is Object), + is Interface where instance is Object, + is CustomScalarType: + try instance.set(value: parsedValue, forField: self) + // TODO: This should not trigger objects to become dirty. + +// case let interface as Interface where instance is Interface: +// try instance.set(value: object, forField: self) +// break // TODO + + case is Interface, is ScalarType: break + default: break + } + } + + private static func object(for instance: ObjectType) -> Object { + switch instance { + case let object as Object: return object + case let interface as Interface: return interface.object + + default: fatalError("AnyCacheObject can only be an Object or a Interface.") + } + } + +// public var projectedValue: CacheField { self } + + @available(*, unavailable, + message: "This property wrapper can only be applied to ObjectType." + ) + public var wrappedValue: T? { get { fatalError() } set { fatalError() } } +} diff --git a/Sources/ApolloAPI/CodegenV1/SchemaTypes/Interface.swift b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Interface.swift new file mode 100644 index 0000000000..2a9adafb69 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Interface.swift @@ -0,0 +1,53 @@ +open class Interface: ObjectType, Cacheable { + + final let object: Object + final var underlyingType: Object.Type { Swift.type(of: object) } // TODO: Delete? + + public static var fields: [String : Cacheable.Type] { [:] } + + public final var _transaction: CacheTransaction { object._transaction } + public final var data: [String: Any] { object.data } + + public required init(_ object: Object) throws { + let objectType = type(of: object) + guard objectType.__metadata.implements(Self.self) else { + throw CacheError.Reason.invalidObjectType(objectType, forExpectedType: Self.self) + } + + self.object = object + } + + public required convenience init(_ interface: Interface) throws { + try self.init(interface.object) + } + + public static func value( + with cacheData: Any, + in transaction: CacheTransaction + ) throws -> Self { + switch cacheData { + case let dataAsSelf as Self: + return dataAsSelf + + case let object as Object: + return try Self(object) + + case let key as CacheKey: + return try Self(transaction.object(withKey: key)!) + + case let data as [String: Any]: + return try Self(transaction.object(withData: data)) + + case let interface as Interface: + return try Self(interface) + + default: + throw CacheError.Reason.unrecognizedCacheData(cacheData, forType: Self.self) // TODO + } + } + + public final func set(value: T?, forField field: Field) throws { + try object.set(value: value, forField: field) + } + +} diff --git a/Sources/ApolloAPI/CodegenV1/SchemaTypes/Object.swift b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Object.swift new file mode 100644 index 0000000000..ef3b7e1271 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Object.swift @@ -0,0 +1,143 @@ +open class Object: ObjectType, Cacheable { + public final let _transaction: CacheTransaction + public internal(set) final var data: [String: Any] + open class var __metadata: Metadata { Metadata.Empty } + open class var __typename: String { UnknownTypeName } + + static let UnknownTypeName: String = "∅__UnknownType" + + final var __typename: String { data["__typename"] as! String } // TODO: delete? + + public required init(transaction: CacheTransaction, data: [String: Any] = [:]) { + self._transaction = transaction + self.data = data + + if self.data["__typename"] == nil { + self.data["__typename"] = Self.__typename + } + } + + public static func value( + with cacheData: Any, + in transaction: CacheTransaction + ) throws -> Self { + let object = try getObject(with: cacheData, in: transaction) + guard let objectAsSelf = object as? Self else { + throw CacheError.Reason.invalidObjectType(type(of: object), forExpectedType: Self.self) + } + return objectAsSelf + } + + private static func getObject( + with cacheData: Any, + in transaction: CacheTransaction + ) throws -> Object { + switch cacheData { + case let object as Object: + return object + + case let key as CacheKey: + guard let object = transaction.object(withKey: key) else { + throw CacheError.Reason.objectNotFound(forCacheKey: key) + } + return object + + case let data as [String: Any]: + return transaction.object(withData: data) + + case let interface as Interface: + return interface.object + + case let union as AnyUnion: + return union.object + + default: + throw CacheError.Reason.unrecognizedCacheData(cacheData, forType: Self.self) + } + } + + public final func set(value: T?, forField field: Field) throws { + let fieldName = field.field.description + + guard let value = value else { + data[fieldName] = nil + return + } + + switch T.self { + case is ObjectType.Type: + // Check for field covariance + if let covariantFieldType = Self.__metadata.fieldTypeIfCovariant(forField: fieldName) { + try set(value: value, forCovariantField: fieldName, convertingToType: covariantFieldType) + + } else { + // data[fieldName] = value // TODO: write tests + } + + // case is ScalarType.Type: + // break + // case is CustomScalarType.Type: + // break + // case is GraphQLEnum.Type: + // break + default: break + } + } + + private func set( + value: Cacheable, + forCovariantField fieldName: String, + convertingToType covariantFieldType: ObjectType.Type + ) throws { + do { + switch covariantFieldType { + case let interfaceType as Interface.Type: + data[fieldName] = try interfaceType.value(with: value, in: _transaction) + + case let objectType as Object.Type: + data[fieldName] = try objectType.value(with: value, in: _transaction) + + default: break // TODO: throw error or fatal error? + } + + } catch { + throw CacheError.Reason.invalidValue(value, forCovariantFieldOfType: covariantFieldType) + } + } +} + +extension Object { + public struct Metadata { + private let implementedInterfaces: [Interface.Type]? + private let covariantFields: [String: ObjectType.Type]? + + fileprivate static let Empty = Metadata() + + public init(implements: [Interface.Type]? = nil, + covariantFields: [String: ObjectType.Type]? = nil) { + self.implementedInterfaces = implements + self.covariantFields = covariantFields + } + + func fieldTypeIfCovariant(forField field: String) -> ObjectType.Type? { + covariantFields?[field] + } + + func implements(_ interface: Interface.Type) -> Bool { + implementedInterfaces?.contains(where: { $0 == interface }) ?? false + } + } +} + +public enum MockError: Error { + case mock // TODO +} + +/// Represents a key that references a record in the cache. +public struct CacheKey: Hashable { + public let key: String + + public init(_ key: String) { + self.key = key + } +} diff --git a/Sources/ApolloAPI/CodegenV1/SchemaTypes/ObjectType.swift b/Sources/ApolloAPI/CodegenV1/SchemaTypes/ObjectType.swift new file mode 100644 index 0000000000..68e9f0fcf8 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/SchemaTypes/ObjectType.swift @@ -0,0 +1,6 @@ +public protocol ObjectType: Cacheable { + var _transaction: CacheTransaction { get } + var data: [String: Any] { get } + + func set(value: T?, forField field: Field) throws +} diff --git a/Sources/ApolloAPI/CodegenV1/SchemaTypes/Union.swift b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Union.swift new file mode 100644 index 0000000000..a0ebdeac18 --- /dev/null +++ b/Sources/ApolloAPI/CodegenV1/SchemaTypes/Union.swift @@ -0,0 +1,177 @@ +protocol AnyUnion: ObjectType { + var object: Object { get } +} + +// MARK: - UnionType +public protocol UnionType { + static var possibleTypes: [Object.Type] { get } + var object: Object { get } + + init?(_ object: Object) +} + +public enum Union: AnyUnion, Equatable { + + case `case`(T) + case __unknown(Object) + + init(_ object: Object) throws { + guard let value = T.init(object) else { + let objectType = type(of: object) + guard objectType != Object.self else { + self = .__unknown(object) + return + } + + throw CacheError.Reason.invalidObjectType(type(of: object), forExpectedType: Self.self) + } + + self = .case(value) + } + + public static func value( + with cacheData: Any, + in transaction: CacheTransaction + ) throws -> Self { + guard let object = object(with: cacheData, in: transaction) else { + throw CacheError.Reason.unrecognizedCacheData(cacheData, forType: T.self) + } + + return try Self(object) + } + + private static func object( + with cacheData: Any, + in transaction: CacheTransaction + ) -> Object? { + switch cacheData { + case let object as Object: return object + case let interface as Interface: return interface.object + case let key as CacheKey: return transaction.object(withKey: key) + case let data as [String: Any]: return transaction.object(withData: data) + default: return nil + } + } + + var value: T? { + switch self { + case let .case(value): return value + default: return nil + } + } + + var object: Object { + switch self { + case let .case(value): return value.object + case let .__unknown(object): return object + } + } + + public var _transaction: CacheTransaction { object._transaction } + public var data: [String : Any] { object.data } + + public func set(value: T?, forField field: Field) throws { + try object.set(value: value, forField: field) + } +} + +// MARK: Union Equatable +extension Union { + public static func ==(lhs: Union, rhs: Union) -> Bool { + return lhs.object === rhs.object + } + + public static func ==(lhs: Union, rhs: T) -> Bool { + return lhs.object === rhs.object + } + + public static func ==(lhs: Union, rhs: Object) -> Bool { + return lhs.object === rhs + } + + public static func !=(lhs: Union, rhs: Union) -> Bool { + return lhs.object !== rhs.object + } + + public static func !=(lhs: Union, rhs: T) -> Bool { + return lhs.object !== rhs.object + } + + public static func !=(lhs: Union, rhs: Object) -> Bool { + return lhs.object !== rhs + } +} + +// MARK: Optional> Equatable + +public func ==(lhs: Union?, rhs: Union) -> Bool { + return lhs?.object === rhs.object +} + +public func ==(lhs: Union?, rhs: T) -> Bool { + return lhs?.object === rhs.object +} + +public func ==(lhs: Union?, rhs: Object) -> Bool { + return lhs?.object === rhs +} + +public func !=(lhs: Union?, rhs: Union) -> Bool { + return lhs?.object !== rhs.object +} + +public func !=(lhs: Union?, rhs: T) -> Bool { + return lhs?.object !== rhs.object +} + +public func !=(lhs: Union?, rhs: Object) -> Bool { + return lhs?.object !== rhs +} + +// MARK: Union Pattern Matching Helpers +extension Union { + public static func ~=(lhs: T, rhs: Union) -> Bool { + switch rhs { + case let .case(rhs) where rhs.object === lhs.object: return true + case let .__unknown(rhsObject) where rhsObject === lhs.object: return true + default: return false + } + } +} + +// MARK: UnionType Equatable +extension UnionType where Self: Equatable { + public static func ==(lhs: Self, rhs: Self) -> Bool { + lhs.object === rhs.object + } + + public static func ==(lhs: Self, rhs: Object) -> Bool { + lhs.object === rhs + } + + public static func !=(lhs: Self, rhs: Self) -> Bool { + lhs.object !== rhs.object + } + + public static func !=(lhs: Self, rhs: Object) -> Bool { + lhs.object !== rhs + } +} + +// MARK: Optional Equatable + +public func ==(lhs: T?, rhs: T) -> Bool { + return lhs?.object === rhs.object +} + +public func !=(lhs: T?, rhs: T) -> Bool { + return lhs?.object !== rhs.object +} + +public func ==(lhs: T?, rhs: Object) -> Bool { + return lhs?.object === rhs +} + +public func !=(lhs: T?, rhs: Object) -> Bool { + return lhs?.object !== rhs +} diff --git a/Sources/ApolloAPI/CodegenV1/SelectionSet.swift b/Sources/ApolloAPI/CodegenV1/SelectionSet.swift index de7e3a4df5..be2819313a 100644 --- a/Sources/ApolloAPI/CodegenV1/SelectionSet.swift +++ b/Sources/ApolloAPI/CodegenV1/SelectionSet.swift @@ -1,26 +1,26 @@ -public enum SelectionSetType { - case ObjectType(S.ObjectType) - case Interface(S.Interface) - case Union(S.Union) -} - public protocol AnySelectionSet: ResponseObject { static var selections: [Selection] { get } } -public protocol SelectionSet: ResponseObject, Equatable { +public enum ParentType { + case Object(Object.Type) + case Interface(Interface.Type) + case Union(UnionType.Type) +} + +public protocol SelectionSet: ResponseObject { - associatedtype Schema: GraphQLSchema + associatedtype Schema: SchemaTypeFactory /// The GraphQL type for the `SelectionSet`. /// - /// This may be a concrete type (`ConcreteType`) or an abstract type (`Interface`). - static var __parentType: SelectionSetType { get } + /// This may be a concrete type (`Object`) or an abstract type (`Interface`, or `Union`). + static var __parentType: ParentType { get } } extension SelectionSet { - var __objectType: Schema.ObjectType { Schema.ObjectType(rawValue: __typename) ?? .unknownCase } + var __objectType: Object.Type? { Schema.objectType(forTypename: __typename) } var __typename: String { data["__typename"] } @@ -31,27 +31,23 @@ extension SelectionSet { /// Generated call sites are guaranteed by the GraphQL compiler to be safe. /// Unsupported usage may result in unintended consequences including crashes. func _asType() -> T? where T.Schema == Schema { - guard case let __objectType = __objectType, __objectType != .unknownCase else { return nil } + guard let __objectType = __objectType else { return nil } switch T.__parentType { - case .ObjectType(let type): + case .Object(let type): guard __objectType == type else { return nil } case .Interface(let interface): - guard __objectType.implements(interface) else { return nil } + guard __objectType.__metadata.implements(interface) else { return nil } case .Union(let union): - guard union.possibleTypes.contains(__objectType) else { return nil } + guard union.possibleTypes.contains(where: { $0 == __objectType }) else { return nil } } return T.init(data: data) } } -func ==(lhs: T, rhs: T) -> Bool { - return true // TODO: Unit test & implement this -} - public protocol ResponseObject { var data: ResponseDict { get } diff --git a/Sources/ApolloAPI/ScalarTypes.swift b/Sources/ApolloAPI/ScalarTypes.swift index 9ace4f446d..a3c66472ff 100644 --- a/Sources/ApolloAPI/ScalarTypes.swift +++ b/Sources/ApolloAPI/ScalarTypes.swift @@ -1,7 +1,24 @@ -public protocol ScalarType {} +public protocol ScalarType: Cacheable {} extension String: ScalarType {} extension Int: ScalarType {} +extension Bool: ScalarType {} extension Float: ScalarType {} extension Double: ScalarType {} -extension Bool: ScalarType {} + +extension ScalarType { + public static func value(with cacheData: Any, in transaction: CacheTransaction) throws -> Self { + return cacheData as! Self + } +} + +public protocol CustomScalarType: Cacheable { + init(scalarData: Any) throws + var jsonValue: Any { get } +} + +extension CustomScalarType { + public static func value(with cacheData: Any, in: CacheTransaction) throws -> Self { + try Self.init(scalarData: cacheData) + } +}