diff --git a/Package.resolved b/Package.resolved index d84a2e53..21093ae0 100644 --- a/Package.resolved +++ b/Package.resolved @@ -5,17 +5,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/GraphQLSwift/GraphQL.git", "state" : { - "revision" : "5e098b3b1789169dded5116c171f716d584225ea", - "version" : "3.0.0" - } - }, - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" + "revision" : "eedec2bbfcfd0c10c2eaee8ac2f91bde5af28b8c", + "version" : "4.0.0" } }, { @@ -23,26 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-collections", "state" : { - "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", - "version" : "1.1.4" - } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "f7dc3f527576c398709b017584392fb58592e7f5", - "version" : "2.75.0" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", - "version" : "1.4.0" + "revision" : "8c0c0a8b49e080e54e5e328cc552821ff07cd341", + "version" : "1.2.1" } } ], diff --git a/Package.swift b/Package.swift index 26346f1e..eac29274 100644 --- a/Package.swift +++ b/Package.swift @@ -3,16 +3,18 @@ import PackageDescription let package = Package( name: "Graphiti", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .library(name: "Graphiti", targets: ["Graphiti"]), ], dependencies: [ - .package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "3.0.0"), + .package(url: "https://github.com/GraphQLSwift/GraphQL.git", from: "4.0.0"), ], targets: [ .target(name: "Graphiti", dependencies: ["GraphQL"]), .testTarget(name: "GraphitiTests", dependencies: ["Graphiti"], resources: [ .copy("FederationTests/GraphQL"), ]), - ] + ], + swiftLanguageVersions: [.v5, .version("6")] ) diff --git a/README.md b/README.md index e9eeef12..b2efcb6d 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Graphiti +# Graphiti Graphiti is a Swift library for building GraphQL schemas fast, safely and easily. @@ -86,7 +86,7 @@ struct MessageAPI : API { let resolver: Resolver let schema: Schema } - + let api = MessageAPI( resolver: Resolver() schema: try! Schema { @@ -124,7 +124,7 @@ let api = MessageAPI( resolver: Resolver() schema: schema ) -``` +```
@@ -136,7 +136,7 @@ final class ChatSchema: PartialSchema { public override var types: Types { Type(Message.self) { Field("content", at: \.content) - } + } } @FieldDefinitions @@ -152,7 +152,7 @@ let api = MessageAPI( resolver: Resolver() schema: schema ) -``` +```
@@ -164,7 +164,7 @@ let chatSchema = PartialSchema( types: { Type(Message.self) { Field("content", at: \.content) - } + } }, query: { Field("message", at: Resolver.message) @@ -178,7 +178,7 @@ let api = MessageAPI( resolver: Resolver() schema: schema ) -``` +``` @@ -190,20 +190,10 @@ let api = MessageAPI( #### Querying -To query the schema we need to pass in a NIO EventLoopGroup to feed the execute function alongside the query itself. - ```swift -import NIO - -let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) -defer { - try? group.syncShutdownGracefully() -} - let result = try await api.execute( request: "{ message { content } }", - context: Context(), - on: group + context: Context() ) print(result) ``` @@ -228,27 +218,13 @@ struct Resolver { } ``` -#### NIO resolvers - -The resolver functions also support `NIO`-style concurrency. To do so, just add one more parameter with type `EventLoopGroup` to the resolver function and change the return type to `EventLoopFuture`. Don't forget to import NIO. - -```swift -import NIO - -struct Resolver { - func message(context: Context, arguments: NoArguments, group: EventLoopGroup) -> EventLoopFuture { - group.next().makeSucceededFuture(context.message()) - } -} -``` - #### Subscription This library supports GraphQL subscriptions, and supports them through the Swift Concurrency `AsyncThrowingStream` type. See the [Usage Guide](UsageGuide.md#subscriptions) for details. If you are unable to use Swift Concurrency, you must create a concrete subclass of the `EventStream` class that implements event streaming functionality. If you don't feel like creating a subclass yourself, you can use the [GraphQLRxSwift](https://github.com/GraphQLSwift/GraphQLRxSwift) repository -to integrate [RxSwift](https://github.com/ReactiveX/RxSwift) observables out-of-the-box. Or you can use that repository as a reference to connect a different +to integrate [RxSwift](https://github.com/ReactiveX/RxSwift) observables out-of-the-box. Or you can use that repository as a reference to connect a different stream library like [ReactiveSwift](https://github.com/ReactiveCocoa/ReactiveSwift), [OpenCombine](https://github.com/OpenCombine/OpenCombine), or one that you've created yourself. diff --git a/Sources/Graphiti/API/API.swift b/Sources/Graphiti/API/API.swift index 32dc1490..51a0b9b9 100644 --- a/Sources/Graphiti/API/API.swift +++ b/Sources/Graphiti/API/API.swift @@ -1,9 +1,8 @@ import GraphQL -import NIO public protocol API { - associatedtype Resolver - associatedtype ContextType + associatedtype Resolver: Sendable + associatedtype ContextType: Sendable var resolver: Resolver { get } var schema: Schema { get } } @@ -12,143 +11,59 @@ public extension API { func execute( request: String, context: ContextType, - on eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil, - validationRules: [(ValidationContext) -> Visitor] = [] - ) -> EventLoopFuture { - return schema.execute( - request: request, - resolver: resolver, - context: context, - eventLoopGroup: eventLoopGroup, - variables: variables, - operationName: operationName, - validationRules: validationRules - ) - } - - func execute( - request: GraphQLRequest, - context: ContextType, - on eventLoopGroup: EventLoopGroup, - validationRules: [(ValidationContext) -> Visitor] = [] - ) -> EventLoopFuture { - return execute( - request: request.query, - context: context, - on: eventLoopGroup, - variables: request.variables, - operationName: request.operationName, - validationRules: validationRules - ) - } - - func subscribe( - request: String, - context: ContextType, - on eventLoopGroup: EventLoopGroup, - variables: [String: Map] = [:], - operationName: String? = nil, - validationRules: [(ValidationContext) -> Visitor] = [] - ) -> EventLoopFuture { - return schema.subscribe( - request: request, - resolver: resolver, - context: context, - eventLoopGroup: eventLoopGroup, - variables: variables, - operationName: operationName, - validationRules: validationRules - ) - } - - func subscribe( - request: GraphQLRequest, - context: ContextType, - on eventLoopGroup: EventLoopGroup, - validationRules: [(ValidationContext) -> Visitor] = [] - ) -> EventLoopFuture { - return subscribe( - request: request.query, - context: context, - on: eventLoopGroup, - variables: request.variables, - operationName: request.operationName, - validationRules: validationRules - ) - } -} - -public extension API { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - func execute( - request: String, - context: ContextType, - on eventLoopGroup: EventLoopGroup, - variables: [String: Map] = [:], - operationName: String? = nil, - validationRules: [(ValidationContext) -> Visitor] = [] + validationRules: [@Sendable (ValidationContext) -> Visitor] = [] ) async throws -> GraphQLResult { return try await schema.execute( request: request, resolver: resolver, context: context, - eventLoopGroup: eventLoopGroup, variables: variables, operationName: operationName, validationRules: validationRules - ).get() + ) } - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) func execute( request: GraphQLRequest, context: ContextType, - on eventLoopGroup: EventLoopGroup, - validationRules: [(ValidationContext) -> Visitor] = [] + validationRules: [@Sendable (ValidationContext) -> Visitor] = [] ) async throws -> GraphQLResult { return try await execute( request: request.query, context: context, - on: eventLoopGroup, variables: request.variables, operationName: request.operationName, validationRules: validationRules ) } - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) func subscribe( request: String, context: ContextType, - on eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil, - validationRules: [(ValidationContext) -> Visitor] = [] - ) async throws -> SubscriptionResult { + validationRules: [@Sendable (ValidationContext) -> Visitor] = [] + ) async throws -> Result, GraphQLErrors> { return try await schema.subscribe( request: request, resolver: resolver, context: context, - eventLoopGroup: eventLoopGroup, variables: variables, operationName: operationName, validationRules: validationRules - ).get() + ) } - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) func subscribe( request: GraphQLRequest, context: ContextType, - on eventLoopGroup: EventLoopGroup, - validationRules: [(ValidationContext) -> Visitor] = [] - ) async throws -> SubscriptionResult { + validationRules: [@Sendable (ValidationContext) -> Visitor] = [] + ) async throws -> Result, GraphQLErrors> { return try await subscribe( request: request.query, context: context, - on: eventLoopGroup, variables: request.variables, operationName: request.operationName, validationRules: validationRules diff --git a/Sources/Graphiti/Argument/Argument.swift b/Sources/Graphiti/Argument/Argument.swift index 737e1f99..26ef7b14 100644 --- a/Sources/Graphiti/Argument/Argument.swift +++ b/Sources/Graphiti/Argument/Argument.swift @@ -2,7 +2,7 @@ import GraphQL public class Argument: ArgumentComponent { let name: String - var defaultValue: AnyEncodable? = nil + var defaultValue: AnyEncodable? override func argument( typeProvider: TypeProvider, diff --git a/Sources/Graphiti/Argument/ArgumentComponent.swift b/Sources/Graphiti/Argument/ArgumentComponent.swift index 0d8163ad..11cfbd92 100644 --- a/Sources/Graphiti/Argument/ArgumentComponent.swift +++ b/Sources/Graphiti/Argument/ArgumentComponent.swift @@ -1,7 +1,7 @@ import GraphQL public class ArgumentComponent { - var description: String? = nil + var description: String? func argument( typeProvider _: TypeProvider, diff --git a/Sources/Graphiti/Argument/NoArguments.swift b/Sources/Graphiti/Argument/NoArguments.swift index fa677dd7..354e3ee5 100644 --- a/Sources/Graphiti/Argument/NoArguments.swift +++ b/Sources/Graphiti/Argument/NoArguments.swift @@ -1,3 +1,3 @@ -public struct NoArguments: Decodable { +public struct NoArguments: Decodable, Sendable { public init() {} } diff --git a/Sources/Graphiti/Coder/Coders.swift b/Sources/Graphiti/Coder/Coders.swift index 6a3eec23..ce8e69b6 100644 --- a/Sources/Graphiti/Coder/Coders.swift +++ b/Sources/Graphiti/Coder/Coders.swift @@ -3,7 +3,7 @@ import GraphQL /// Struct containing a MapEncoder and MapDecoder. These decoders are passed through to the Schema objects and used in /// all encoding and decoding from maps. -public struct Coders { +public struct Coders: Sendable { public let decoder = MapDecoder() public let encoder = MapEncoder() diff --git a/Sources/Graphiti/Component/Component.swift b/Sources/Graphiti/Component/Component.swift index 2cb031ab..3836e316 100644 --- a/Sources/Graphiti/Component/Component.swift +++ b/Sources/Graphiti/Component/Component.swift @@ -1,6 +1,6 @@ import GraphQL -open class Component { +open class Component { let name: String var description: String? var componentType: ComponentType diff --git a/Sources/Graphiti/Connection/Connection.swift b/Sources/Graphiti/Connection/Connection.swift index 7a94b490..6ff5c84b 100644 --- a/Sources/Graphiti/Connection/Connection.swift +++ b/Sources/Graphiti/Connection/Connection.swift @@ -1,13 +1,11 @@ import Foundation import GraphQL -import NIO -public struct Connection { +public struct Connection: Sendable { public let edges: [Edge] public let pageInfo: PageInfo } -@available(macOS 10.15, macCatalyst 13.0, iOS 13.0, tvOS 13, watchOS 6.0, *) // For Identifiable public extension Connection where Node: Identifiable, Node.ID: LosslessStringConvertible { static func id(_ cursor: String) -> Node.ID? { cursor.base64Decoded().flatMap { Node.ID($0) } @@ -18,55 +16,7 @@ public extension Connection where Node: Identifiable, Node.ID: LosslessStringCon } } -@available(macOS 10.15, macCatalyst 13.0, iOS 13.0, tvOS 13, watchOS 6.0, *) // For Identifiable -public extension EventLoopFuture where Value: Sequence, Value.Element: Identifiable, -Value.Element.ID: LosslessStringConvertible { - func connection(from arguments: Paginatable) -> EventLoopFuture> { - connection(from: arguments, makeCursor: Connection.cursor) - } - - func connection(from arguments: ForwardPaginatable) - -> EventLoopFuture> { - connection(from: arguments, makeCursor: Connection.cursor) - } - - func connection(from arguments: BackwardPaginatable) - -> EventLoopFuture> { - connection(from: arguments, makeCursor: Connection.cursor) - } -} - -public extension EventLoopFuture where Value: Sequence { - func connection( - from arguments: Paginatable, - makeCursor: @escaping (Value.Element) throws -> String - ) -> EventLoopFuture> { - flatMapThrowing { value in - try value.connection(from: arguments, makeCursor: makeCursor) - } - } - - func connection( - from arguments: ForwardPaginatable, - makeCursor: @escaping (Value.Element) throws -> String - ) -> EventLoopFuture> { - flatMapThrowing { value in - try value.connection(from: arguments, makeCursor: makeCursor) - } - } - - func connection( - from arguments: BackwardPaginatable, - makeCursor: @escaping (Value.Element) throws -> String - ) -> EventLoopFuture> { - flatMapThrowing { value in - try value.connection(from: arguments, makeCursor: makeCursor) - } - } -} - -@available(macOS 10.15, macCatalyst 13.0, iOS 13.0, tvOS 13, watchOS 6.0, *) // For Identifiable -public extension Sequence where Element: Identifiable, +public extension Sequence where Element: Sendable, Element: Identifiable, Element.ID: LosslessStringConvertible { func connection(from arguments: Paginatable) throws -> Connection { try connection(from: arguments, makeCursor: Connection.cursor) @@ -81,7 +31,7 @@ Element.ID: LosslessStringConvertible { } } -public extension Sequence { +public extension Sequence where Element: Sendable { func connection( from arguments: Paginatable, makeCursor: @escaping (Element) throws -> String diff --git a/Sources/Graphiti/Connection/ConnectionType.swift b/Sources/Graphiti/Connection/ConnectionType.swift index 5f8faa9e..b3e87ab0 100644 --- a/Sources/Graphiti/Connection/ConnectionType.swift +++ b/Sources/Graphiti/Connection/ConnectionType.swift @@ -1,9 +1,9 @@ import GraphQL public final class ConnectionType< - Resolver, - Context, - ObjectType + Resolver: Sendable, + Context: Sendable, + ObjectType: Sendable >: TypeComponent< Resolver, Context diff --git a/Sources/Graphiti/Connection/Edge.swift b/Sources/Graphiti/Connection/Edge.swift index 0ab61c26..daf4c79a 100644 --- a/Sources/Graphiti/Connection/Edge.swift +++ b/Sources/Graphiti/Connection/Edge.swift @@ -1,10 +1,10 @@ -protocol Edgeable { +protocol Edgeable: Sendable { associatedtype Node var node: Node { get } var cursor: String { get } } -public struct Edge: Edgeable { +public struct Edge: Edgeable { public let node: Node public let cursor: String } diff --git a/Sources/Graphiti/Connection/PageInfo.swift b/Sources/Graphiti/Connection/PageInfo.swift index 72b2bf40..700306ff 100644 --- a/Sources/Graphiti/Connection/PageInfo.swift +++ b/Sources/Graphiti/Connection/PageInfo.swift @@ -1,4 +1,4 @@ -public struct PageInfo: Codable { +public struct PageInfo: Codable, Sendable { public let hasPreviousPage: Bool public let hasNextPage: Bool public let startCursor: String? diff --git a/Sources/Graphiti/Connection/PagniationArguments/PaginationArguments.swift b/Sources/Graphiti/Connection/PagniationArguments/PaginationArguments.swift index 1ec443cd..4ec1e123 100644 --- a/Sources/Graphiti/Connection/PagniationArguments/PaginationArguments.swift +++ b/Sources/Graphiti/Connection/PagniationArguments/PaginationArguments.swift @@ -1,6 +1,6 @@ public protocol Paginatable: ForwardPaginatable, BackwardPaginatable {} -public struct PaginationArguments: Paginatable { +public struct PaginationArguments: Paginatable, Sendable { public let first: Int? public let last: Int? public let after: String? diff --git a/Sources/Graphiti/Enum/Enum.swift b/Sources/Graphiti/Enum/Enum.swift index f32fc988..7012792f 100644 --- a/Sources/Graphiti/Enum/Enum.swift +++ b/Sources/Graphiti/Enum/Enum.swift @@ -1,8 +1,8 @@ import GraphQL public final class Enum< - Resolver, - Context, + Resolver: Sendable, + Context: Sendable, EnumType: Encodable & RawRepresentable >: TypeComponent< Resolver, diff --git a/Sources/Graphiti/Federation/Entity.swift b/Sources/Graphiti/Federation/Entity.swift index eaf21cfe..fbe1f130 100644 --- a/Sources/Graphiti/Federation/Entity.swift +++ b/Sources/Graphiti/Federation/Entity.swift @@ -1,6 +1,5 @@ import Foundation import GraphQL -import NIO struct EntityArguments: Codable { let representations: [Map] diff --git a/Sources/Graphiti/Federation/Key/Key.swift b/Sources/Graphiti/Federation/Key/Key.swift index e05c8e68..70877eae 100644 --- a/Sources/Graphiti/Federation/Key/Key.swift +++ b/Sources/Graphiti/Federation/Key/Key.swift @@ -1,11 +1,15 @@ import GraphQL -import NIO -public class Key: KeyComponent< +public class Key< + ObjectType: Sendable, + Resolver: Sendable, + Context: Sendable, + Arguments: Codable & Sendable +>: KeyComponent< ObjectType, Resolver, Context -> { +>, @unchecked Sendable { let arguments: [ArgumentComponent] let resolve: AsyncResolve @@ -18,11 +22,10 @@ public class Key: KeyComponen resolver: Resolver, context: Context, map: Map, - eventLoopGroup: EventLoopGroup, coders: Coders - ) throws -> EventLoopFuture { + ) async throws -> (any Sendable)? { let arguments = try coders.decoder.decode(Arguments.self, from: map) - return try resolve(resolver)(context, arguments, eventLoopGroup).map { $0 as Any? } + return try await resolve(resolver)(context, arguments) } override func validate( @@ -55,61 +58,16 @@ public class Key: KeyComponen resolve = asyncResolve } - convenience init( - arguments: [ArgumentComponent], - simpleAsyncResolve: @escaping SimpleAsyncResolve< - Resolver, - Context, - Arguments, - ObjectType? - > - ) { - let asyncResolve: AsyncResolve = { type in - { context, arguments, group in - // We hop to guarantee that the future will - // return in the same event loop group of the execution. - try simpleAsyncResolve(type)(context, arguments).hop(to: group.next()) - } - } - - self.init(arguments: arguments, asyncResolve: asyncResolve) - } - convenience init( arguments: [ArgumentComponent], syncResolve: @escaping SyncResolve ) { let asyncResolve: AsyncResolve = { type in - { context, arguments, group in - let result = try syncResolve(type)(context, arguments) - return group.next().makeSucceededFuture(result) + { context, arguments in + try syncResolve(type)(context, arguments) } } self.init(arguments: arguments, asyncResolve: asyncResolve) } } - -public extension Key { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - arguments: [ArgumentComponent], - concurrentResolve: @escaping ConcurrentResolve< - Resolver, - Context, - Arguments, - ObjectType? - > - ) { - let asyncResolve: AsyncResolve = { type in - { context, arguments, eventLoopGroup in - let promise = eventLoopGroup.next().makePromise(of: ObjectType?.self) - promise.completeWithTask { - try await concurrentResolve(type)(context, arguments) - } - return promise.futureResult - } - } - self.init(arguments: arguments, asyncResolve: asyncResolve) - } -} diff --git a/Sources/Graphiti/Federation/Key/KeyComponent.swift b/Sources/Graphiti/Federation/Key/KeyComponent.swift index 0bb51bc4..b739cb79 100644 --- a/Sources/Graphiti/Federation/Key/KeyComponent.swift +++ b/Sources/Graphiti/Federation/Key/KeyComponent.swift @@ -1,7 +1,6 @@ import GraphQL -import NIO -public class KeyComponent { +public class KeyComponent: @unchecked Sendable { func mapMatchesArguments(_: Map, coders _: Coders) -> Bool { fatalError() } @@ -10,9 +9,8 @@ public class KeyComponent { resolver _: Resolver, context _: Context, map _: Map, - eventLoopGroup _: EventLoopGroup, coders _: Coders - ) throws -> EventLoopFuture { + ) async throws -> (any Sendable)? { fatalError() } diff --git a/Sources/Graphiti/Federation/Key/Type+Key.swift b/Sources/Graphiti/Federation/Key/Type+Key.swift index d1e8443c..2f5128f3 100644 --- a/Sources/Graphiti/Federation/Key/Type+Key.swift +++ b/Sources/Graphiti/Federation/Key/Type+Key.swift @@ -9,7 +9,7 @@ public extension Type { /// - _: The key value. The name of this argument must match a Type field. /// - Returns: Self for chaining. @discardableResult - func key( + func key( at function: @escaping AsyncResolve, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) -> Self { @@ -25,7 +25,7 @@ public extension Type { /// - _: The key values. The names of these arguments must match Type fields. /// - Returns: Self for chaining. @discardableResult - func key( + func key( at function: @escaping AsyncResolve, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -42,40 +42,7 @@ public extension Type { /// - _: The key value. The name of this argument must match a Type field. /// - Returns: Self for chaining. @discardableResult - func key( - at function: @escaping SimpleAsyncResolve, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) -> Self { - keys.append(Key(arguments: [argument()], simpleAsyncResolve: function)) - return self - } - - /// Define and add the federated key to this type. - /// - /// For more information, see https://www.apollographql.com/docs/federation/entities - /// - Parameters: - /// - function: The resolver function used to load this entity based on the key value. - /// - _: The key values. The names of these arguments must match Type fields. - /// - Returns: Self for chaining. - @discardableResult - func key( - at function: @escaping SimpleAsyncResolve, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) -> Self { - keys.append(Key(arguments: arguments(), simpleAsyncResolve: function)) - return self - } - - /// Define and add the federated key to this type. - /// - /// For more information, see https://www.apollographql.com/docs/federation/entities - /// - Parameters: - /// - function: The resolver function used to load this entity based on the key value. - /// - _: The key value. The name of this argument must match a Type field. - /// - Returns: Self for chaining. - @discardableResult - func key( + func key( at function: @escaping SyncResolve, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -92,7 +59,7 @@ public extension Type { /// - _: The key values. The names of these arguments must match Type fields. /// - Returns: Self for chaining. @discardableResult - func key( + func key( at function: @escaping SyncResolve, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) -> Self { @@ -100,39 +67,3 @@ public extension Type { return self } } - -public extension Type { - /// Define and add the federated key to this type. - /// - /// For more information, see https://www.apollographql.com/docs/federation/entities - /// - Parameters: - /// - function: The resolver function used to load this entity based on the key value. - /// - _: The key value. The name of this argument must match a Type field. - /// - Returns: Self for chaining. - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - @discardableResult - func key( - at function: @escaping ConcurrentResolve, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) -> Self { - keys.append(Key(arguments: [argument()], concurrentResolve: function)) - return self - } - - /// Define and add the federated key to this type. - /// - /// For more information, see https://www.apollographql.com/docs/federation/entities - /// - Parameters: - /// - function: The resolver function used to load this entity based on the key value. - /// - _: The key values. The names of these arguments must match Type fields. - /// - Returns: Self for chaining. - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - @discardableResult - func key( - at function: @escaping ConcurrentResolve, - @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] - ) -> Self { - keys.append(Key(arguments: arguments(), concurrentResolve: function)) - return self - } -} diff --git a/Sources/Graphiti/Federation/Queries.swift b/Sources/Graphiti/Federation/Queries.swift index 15f5dd1e..c622ba4d 100644 --- a/Sources/Graphiti/Federation/Queries.swift +++ b/Sources/Graphiti/Federation/Queries.swift @@ -1,13 +1,12 @@ import GraphQL -import NIO func serviceQuery(for sdl: String) -> GraphQLField { return GraphQLField( type: GraphQLNonNull(serviceType), description: "Return the SDL string for the subschema", - resolve: { _, _, _, eventLoopGroup, _ in + resolve: { _, _, _, _ in let result = Service(sdl: sdl) - return eventLoopGroup.any().makeSucceededFuture(result) + return result } ) } @@ -21,32 +20,39 @@ func entitiesQuery( type: GraphQLNonNull(GraphQLList(entityType)), description: "Return all entities matching the provided representations.", args: [ - "representations": GraphQLArgument(type: GraphQLNonNull(GraphQLList(GraphQLNonNull(anyType)))), + "representations": GraphQLArgument( + type: GraphQLNonNull(GraphQLList(GraphQLNonNull(anyType))) + ), ], - resolve: { source, args, context, eventLoopGroup, info in + resolve: { source, args, context, info in let arguments = try coders.decoder.decode(EntityArguments.self, from: args) - let futures: [EventLoopFuture] = try arguments.representations - .map { (representationMap: Map) in - let representation = try coders.decoder.decode( - EntityRepresentation.self, - from: representationMap - ) - guard let resolve = federatedResolvers[representation.__typename] else { - throw GraphQLError( - message: "Federated type not found: \(representation.__typename)" + return try await withThrowingTaskGroup(of: (Int, (any Sendable)?).self) { group in + var results: [(any Sendable)?] = arguments.representations.map { _ in nil } + for (index, representationMap) in arguments.representations.enumerated() { + group.addTask { + let representation = try coders.decoder.decode( + EntityRepresentation.self, + from: representationMap ) + guard let resolve = federatedResolvers[representation.__typename] else { + throw GraphQLError( + message: "Federated type not found: \(representation.__typename)" + ) + } + let result = try await resolve( + source, + representationMap, + context, + info + ) + return (index, result) } - return try resolve( - source, - representationMap, - context, - eventLoopGroup, - info - ) } - - return futures.flatten(on: eventLoopGroup) - .map { $0 as Any? } + for try await result in group { + results[result.0] = result.1 + } + return results + } } ) } diff --git a/Sources/Graphiti/Field/Field/Field.swift b/Sources/Graphiti/Field/Field/Field.swift index 5ba7ea14..8ad50a8d 100644 --- a/Sources/Graphiti/Field/Field/Field.swift +++ b/Sources/Graphiti/Field/Field/Field.swift @@ -1,24 +1,29 @@ import GraphQL -import NIO -public class Field: FieldComponent< +public class Field< + ObjectType: Sendable, + Context: Sendable, + FieldType: Sendable, + Arguments: Decodable & Sendable +>: FieldComponent< ObjectType, Context > { let name: String let arguments: [ArgumentComponent] - let resolve: AsyncResolve + let resolve: AsyncResolve override func field( typeProvider: TypeProvider, coders: Coders ) throws -> (String, GraphQLField) { + let resolve = self.resolve let field = try GraphQLField( type: typeProvider.getOutputType(from: FieldType.self, field: name), description: description, deprecationReason: deprecationReason, args: arguments(typeProvider: typeProvider, coders: coders), - resolve: { source, arguments, context, eventLoopGroup, _ in + resolve: { source, arguments, context, _ in guard let s = source as? ObjectType else { throw GraphQLError( message: "Expected source type \(ObjectType.self) but got \(type(of: source))" @@ -32,7 +37,7 @@ public class Field: FieldC } let a = try coders.decoder.decode(Arguments.self, from: arguments) - return try self.resolve(s)(c, a, eventLoopGroup) + return try await resolve(s)(c, a) } ) @@ -53,56 +58,34 @@ public class Field: FieldC init( name: String, arguments: [ArgumentComponent], - resolve: @escaping AsyncResolve + resolve: @escaping AsyncResolve ) { self.name = name self.arguments = arguments self.resolve = resolve } - convenience init( + convenience init( name: String, arguments: [ArgumentComponent], asyncResolve: @escaping AsyncResolve ) { - let resolve: AsyncResolve = { type in - { context, arguments, eventLoopGroup in - try asyncResolve(type)(context, arguments, eventLoopGroup).map { $0 as Any? } + let resolve: AsyncResolve = { type in + { context, arguments in + try await asyncResolve(type)(context, arguments) } } self.init(name: name, arguments: arguments, resolve: resolve) } - convenience init( - name: String, - arguments: [ArgumentComponent], - simpleAsyncResolve: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - ResolveType - > - ) { - let asyncResolve: AsyncResolve = { type in - { context, arguments, group in - // We hop to guarantee that the future will - // return in the same event loop group of the execution. - try simpleAsyncResolve(type)(context, arguments).hop(to: group.next()) - } - } - - self.init(name: name, arguments: arguments, asyncResolve: asyncResolve) - } - - convenience init( + convenience init( name: String, arguments: [ArgumentComponent], syncResolve: @escaping SyncResolve ) { let asyncResolve: AsyncResolve = { type in - { context, arguments, group in - let result = try syncResolve(type)(context, arguments) - return group.next().makeSucceededFuture(result) + { context, arguments in + try syncResolve(type)(context, arguments) } } @@ -131,34 +114,7 @@ public extension Field { } } -// MARK: SimpleAsyncResolve Initializers - -public extension Field { - convenience init( - _ name: String, - at function: @escaping SimpleAsyncResolve, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init(name: name, arguments: [argument()], simpleAsyncResolve: function) - } - - convenience init( - _ name: String, - at function: @escaping SimpleAsyncResolve, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init(name: name, arguments: arguments(), simpleAsyncResolve: function) - } -} - -// MARK: SyncResolve Initializers - -// '@_disfavoredOverload' is included below because otherwise `SimpleAsyncResolve` initializers also match this signature, causing the -// calls to be ambiguous. We prefer that if an EventLoopFuture is returned from the resolve, that `SimpleAsyncResolve` is matched. - public extension Field { - @_disfavoredOverload convenience init( _ name: String, at function: @escaping SyncResolve, @@ -167,7 +123,6 @@ public extension Field { self.init(name: name, arguments: [argument()], syncResolve: function) } - @_disfavoredOverload convenience init( _ name: String, at function: @escaping SyncResolve, @@ -195,50 +150,7 @@ public extension Field where Arguments == NoArguments { } } -public extension Field { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - name: String, - arguments: [ArgumentComponent], - concurrentResolve: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - ResolveType - > - ) { - let asyncResolve: AsyncResolve = { type in - { context, arguments, eventLoopGroup in - let promise = eventLoopGroup.next().makePromise(of: ResolveType.self) - promise.completeWithTask { - try await concurrentResolve(type)(context, arguments) - } - return promise.futureResult - } - } - self.init(name: name, arguments: arguments, asyncResolve: asyncResolve) - } -} - -// MARK: ConcurrentResolve Initializers - -public extension Field { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - at function: @escaping ConcurrentResolve, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init(name: name, arguments: [argument()], concurrentResolve: function) - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - at function: @escaping ConcurrentResolve, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init(name: name, arguments: arguments(), concurrentResolve: function) - } -} +// We must conform KeyPath to unchecked sendable to allow keypath-based resolvers. +// Despite the warning, we cannot add `@retroactive` and keep Swift 5 support. +// Remove when support transitions to Swift 6. +extension KeyPath: @unchecked Sendable {} diff --git a/Sources/Graphiti/Field/Resolve/AsyncResolve.swift b/Sources/Graphiti/Field/Resolve/AsyncResolve.swift index 9f476631..d335a653 100644 --- a/Sources/Graphiti/Field/Resolve/AsyncResolve.swift +++ b/Sources/Graphiti/Field/Resolve/AsyncResolve.swift @@ -1,9 +1,7 @@ -import NIO -public typealias AsyncResolve = ( +public typealias AsyncResolve = @Sendable ( _ object: ObjectType ) -> ( _ context: Context, - _ arguments: Arguments, - _ eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture + _ arguments: Arguments +) async throws -> ResolveType diff --git a/Sources/Graphiti/Field/Resolve/ConcurrentResolve.swift b/Sources/Graphiti/Field/Resolve/ConcurrentResolve.swift deleted file mode 100644 index abfb0704..00000000 --- a/Sources/Graphiti/Field/Resolve/ConcurrentResolve.swift +++ /dev/null @@ -1,9 +0,0 @@ -import NIO - -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -public typealias ConcurrentResolve = ( - _ object: ObjectType -) -> ( - _ context: Context, - _ arguments: Arguments -) async throws -> ResolveType diff --git a/Sources/Graphiti/Field/Resolve/SimpleAsyncResolve.swift b/Sources/Graphiti/Field/Resolve/SimpleAsyncResolve.swift deleted file mode 100644 index 4eaa6db7..00000000 --- a/Sources/Graphiti/Field/Resolve/SimpleAsyncResolve.swift +++ /dev/null @@ -1,8 +0,0 @@ -import NIO - -public typealias SimpleAsyncResolve = ( - _ object: ObjectType -) -> ( - _ context: Context, - _ arguments: Arguments -) throws -> EventLoopFuture diff --git a/Sources/Graphiti/Field/Resolve/SyncResolve.swift b/Sources/Graphiti/Field/Resolve/SyncResolve.swift index 25f02f3a..98a12e32 100644 --- a/Sources/Graphiti/Field/Resolve/SyncResolve.swift +++ b/Sources/Graphiti/Field/Resolve/SyncResolve.swift @@ -1,4 +1,4 @@ -public typealias SyncResolve = ( +public typealias SyncResolve = @Sendable ( _ object: ObjectType ) -> ( _ context: Context, diff --git a/Sources/Graphiti/Input/Input.swift b/Sources/Graphiti/Input/Input.swift index 3b3e21c0..0e8ce2c0 100644 --- a/Sources/Graphiti/Input/Input.swift +++ b/Sources/Graphiti/Input/Input.swift @@ -1,8 +1,8 @@ import GraphQL public final class Input< - Resolver, - Context, + Resolver: Sendable, + Context: Sendable, InputObjectType >: TypeComponent< Resolver, diff --git a/Sources/Graphiti/InputField/InputField.swift b/Sources/Graphiti/InputField/InputField.swift index fb95ea81..93c2431a 100644 --- a/Sources/Graphiti/InputField/InputField.swift +++ b/Sources/Graphiti/InputField/InputField.swift @@ -2,7 +2,7 @@ import GraphQL public class InputField< InputObjectType, - Context, + Context: Sendable, FieldType >: InputFieldComponent< InputObjectType, diff --git a/Sources/Graphiti/Interface/Interface.swift b/Sources/Graphiti/Interface/Interface.swift index a6446daa..08b70df6 100644 --- a/Sources/Graphiti/Interface/Interface.swift +++ b/Sources/Graphiti/Interface/Interface.swift @@ -1,6 +1,10 @@ import GraphQL -public final class Interface: TypeComponent< +public final class Interface< + Resolver: Sendable, + Context: Sendable, + InterfaceType +>: TypeComponent< Resolver, Context > { diff --git a/Sources/Graphiti/Mutation/Mutation.swift b/Sources/Graphiti/Mutation/Mutation.swift index 51f8773e..8f8d92fc 100644 --- a/Sources/Graphiti/Mutation/Mutation.swift +++ b/Sources/Graphiti/Mutation/Mutation.swift @@ -1,9 +1,9 @@ import GraphQL -public final class Mutation: Component { +public final class Mutation: Component { let fields: [FieldComponent] - let isTypeOf: GraphQLIsTypeOf = { source, _, _ in + let isTypeOf: GraphQLIsTypeOf = { source, _ in source is Resolver } diff --git a/Sources/Graphiti/Query/Query.swift b/Sources/Graphiti/Query/Query.swift index 7c257e1d..88f3af67 100644 --- a/Sources/Graphiti/Query/Query.swift +++ b/Sources/Graphiti/Query/Query.swift @@ -1,9 +1,9 @@ import GraphQL -public final class Query: Component { +public final class Query: Component { let fields: [FieldComponent] - let isTypeOf: GraphQLIsTypeOf = { source, _, _ in + let isTypeOf: GraphQLIsTypeOf = { source, _ in source is Resolver } diff --git a/Sources/Graphiti/Scalar/Scalar.swift b/Sources/Graphiti/Scalar/Scalar.swift index 3de7bed2..bde9f3e3 100644 --- a/Sources/Graphiti/Scalar/Scalar.swift +++ b/Sources/Graphiti/Scalar/Scalar.swift @@ -7,44 +7,31 @@ import OrderedCollections /// Encoding/decoding behavior can be modified through the `MapEncoder`/`MapDecoder` options available through /// `Coders` or a custom encoding/decoding on the `ScalarType` itself. If you need very custom serialization controls, /// you may provide your own serialize, parseValue, and parseLiteral implementations. -open class Scalar: TypeComponent { +open class Scalar< + Resolver: Sendable, + Context: Sendable, + ScalarType: Codable +>: TypeComponent { // TODO: Change this no longer be an open class let specifiedByURL: String? override func update(typeProvider: SchemaTypeProvider, coders: Coders) throws { + let serialize = self.serialize + let parseValue = self.parseValue + let parseLiteral = self.parseLiteral + let scalarType = try GraphQLScalarType( name: name, description: description, specifiedByURL: specifiedByURL, serialize: { value in - if let serialize = self.serialize { - return try serialize(value, coders) - } else { - guard let scalar = value as? ScalarType else { - throw GraphQLError( - message: "Serialize expected type \(ScalarType.self) but got \(type(of: value))" - ) - } - - return try self.serialize(scalar: scalar, encoder: coders.encoder) - } + try serialize(value, coders) }, parseValue: { map in - if let parseValue = self.parseValue { - return try parseValue(map, coders) - } else { - let scalar = try self.parse(map: map, decoder: coders.decoder) - return try self.serialize(scalar: scalar, encoder: coders.encoder) - } + try parseValue(map, coders) }, parseLiteral: { value in - if let parseLiteral = self.parseLiteral { - return try parseLiteral(value, coders) - } else { - let map = value.map - let scalar = try self.parse(map: map, decoder: coders.decoder) - return try self.serialize(scalar: scalar, encoder: coders.encoder) - } + try parseLiteral(value, coders) } ) @@ -61,27 +48,50 @@ open class Scalar: TypeComponent Map)? - let parseValue: ((Map, Coders) throws -> Map)? - let parseLiteral: ((GraphQL.Value, Coders) throws -> Map)? + let serialize: @Sendable (Any, Coders) throws -> Map + let parseValue: @Sendable (Map, Coders) throws -> Map + let parseLiteral: @Sendable (GraphQL.Value, Coders) throws -> Map init( type _: ScalarType.Type, name: String?, specifiedBy: String? = nil, - serialize: ((Any, Coders) throws -> Map)? = nil, - parseValue: ((Map, Coders) throws -> Map)? = nil, - parseLiteral: ((GraphQL.Value, Coders) throws -> Map)? = nil + serialize: (@Sendable (Any, Coders) throws -> Map)? = nil, + parseValue: (@Sendable (Map, Coders) throws -> Map)? = nil, + parseLiteral: (@Sendable (GraphQL.Value, Coders) throws -> Map)? = nil ) { specifiedByURL = specifiedBy - self.serialize = serialize - self.parseValue = parseValue - self.parseLiteral = parseLiteral + self.serialize = serialize ?? Self.defaultSerialize + self.parseValue = parseValue ?? Self.defaultParseValue + self.parseLiteral = parseLiteral ?? Self.defaultParseLiteral super.init( name: name ?? Reflection.name(for: ScalarType.self), type: .scalar ) } + + @Sendable + private static func defaultSerialize(value: Any, coders: Coders) throws -> Map { + guard let scalar = value as? ScalarType else { + throw GraphQLError( + message: "Serialize expected type \(ScalarType.self) but got \(type(of: value))" + ) + } + return try coders.encoder.encode(scalar) + } + + @Sendable + private static func defaultParseValue(map: Map, coders: Coders) throws -> Map { + let scalar = try coders.decoder.decode(ScalarType.self, from: map) + return try coders.encoder.encode(scalar) + } + + @Sendable + private static func defaultParseLiteral(value: GraphQL.Value, coders: Coders) throws -> Map { + let map = value.map + let scalar = try coders.decoder.decode(ScalarType.self, from: map) + return try coders.encoder.encode(scalar) + } } public extension Scalar { @@ -89,9 +99,9 @@ public extension Scalar { _ type: ScalarType.Type, as name: String? = nil, specifiedBy: String? = nil, - serialize: ((Any, Coders) throws -> Map)? = nil, - parseValue: ((Map, Coders) throws -> Map)? = nil, - parseLiteral: ((GraphQL.Value, Coders) throws -> Map)? = nil + serialize: (@Sendable (Any, Coders) throws -> Map)? = nil, + parseValue: (@Sendable (Map, Coders) throws -> Map)? = nil, + parseLiteral: (@Sendable (GraphQL.Value, Coders) throws -> Map)? = nil ) { self.init( type: type, diff --git a/Sources/Graphiti/Schema/Schema.swift b/Sources/Graphiti/Schema/Schema.swift index 590b8607..3610571b 100644 --- a/Sources/Graphiti/Schema/Schema.swift +++ b/Sources/Graphiti/Schema/Schema.swift @@ -1,11 +1,10 @@ import GraphQL -import NIO public struct SchemaError: Error, Equatable { let description: String } -public final class Schema { +public final class Schema { public let schema: GraphQLSchema init( @@ -65,49 +64,37 @@ public extension Schema { request: String, resolver: Resolver, context: Context, - eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil, - validationRules: [(ValidationContext) -> Visitor] = [] - ) -> EventLoopFuture { - do { - return try graphql( - validationRules: GraphQL.specifiedRules + validationRules, - schema: schema, - request: request, - rootValue: resolver, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variables, - operationName: operationName - ) - } catch { - return eventLoopGroup.next().makeFailedFuture(error) - } + validationRules: [@Sendable (ValidationContext) -> Visitor] = [] + ) async throws -> GraphQLResult { + return try await graphql( + schema: schema, + request: request, + rootValue: resolver, + context: context, + variableValues: variables, + operationName: operationName, + validationRules: GraphQL.specifiedRules + validationRules + ) } func subscribe( request: String, resolver: Resolver, context: Context, - eventLoopGroup: EventLoopGroup, variables: [String: Map] = [:], operationName: String? = nil, - validationRules: [(ValidationContext) -> Visitor] = [] - ) -> EventLoopFuture { - do { - return try graphqlSubscribe( - validationRules: GraphQL.specifiedRules + validationRules, - schema: schema, - request: request, - rootValue: resolver, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variables, - operationName: operationName - ) - } catch { - return eventLoopGroup.next().makeFailedFuture(error) - } + validationRules: [@Sendable (ValidationContext) -> Visitor] = [] + ) async throws -> Result, GraphQLErrors> { + return try await graphqlSubscribe( + schema: schema, + request: request, + rootValue: resolver, + context: context, + variableValues: variables, + operationName: operationName, + validationRules: GraphQL.specifiedRules + validationRules + ) } } diff --git a/Sources/Graphiti/Schema/SchemaTypeProvider.swift b/Sources/Graphiti/Schema/SchemaTypeProvider.swift index ee57cd00..cadfdac4 100644 --- a/Sources/Graphiti/Schema/SchemaTypeProvider.swift +++ b/Sources/Graphiti/Schema/SchemaTypeProvider.swift @@ -17,7 +17,7 @@ final class SchemaTypeProvider: TypeProvider { var federatedTypes: [GraphQLObjectType] = [] var federatedResolvers: [String: GraphQLFieldResolve] = [:] - var federatedSDL: String? = nil + var federatedSDL: String? var query: GraphQLObjectType? var mutation: GraphQLObjectType? diff --git a/Sources/Graphiti/SchemaBuilders/PartialSchema.swift b/Sources/Graphiti/SchemaBuilders/PartialSchema.swift index c75593ba..af5821c5 100644 --- a/Sources/Graphiti/SchemaBuilders/PartialSchema.swift +++ b/Sources/Graphiti/SchemaBuilders/PartialSchema.swift @@ -1,6 +1,6 @@ /// A partial schema that declare a set of type, query, mutation, and/or subscription definition /// which can be compiled together into 1 schema. -open class PartialSchema { +open class PartialSchema { /// A custom parameter attribute that constructs type definitions from closures. public typealias TypeDefinitions = TypeComponentBuilder diff --git a/Sources/Graphiti/SchemaBuilders/SchemaBuilder.swift b/Sources/Graphiti/SchemaBuilders/SchemaBuilder.swift index 55ea3f8d..721f3ff4 100644 --- a/Sources/Graphiti/SchemaBuilders/SchemaBuilder.swift +++ b/Sources/Graphiti/SchemaBuilders/SchemaBuilder.swift @@ -1,6 +1,6 @@ /// A builder that allows modular creation of GraphQL schemas. You may independently components or query, mutation, or subscription fields. /// When ready to build and use the schema, run `build` -public final class SchemaBuilder { +public final class SchemaBuilder { private var coders: Coders private var federatedSDL: String? private var typeComponents: [TypeComponent] diff --git a/Sources/Graphiti/Subscription/SubscribeField.swift b/Sources/Graphiti/Subscription/SubscribeField.swift index 65baa76d..dbae6565 100644 --- a/Sources/Graphiti/Subscription/SubscribeField.swift +++ b/Sources/Graphiti/Subscription/SubscribeField.swift @@ -1,30 +1,32 @@ import GraphQL -import NIO // Subscription resolver MUST return an Observer, not a specific type, due to lack of support for covariance generics in Swift public class SubscriptionField< - SourceEventType, + SourceEventType: Sendable, ObjectType, Context, - FieldType, - Arguments: Decodable ->: FieldComponent { + FieldType: Sendable, + Arguments: Decodable, + SubSequence: AsyncSequence & Sendable +>: FieldComponent where SubSequence.Element == SourceEventType { let name: String let arguments: [ArgumentComponent] - let resolve: AsyncResolve - let subscribe: AsyncResolve> + let resolve: AsyncResolve + let subscribe: AsyncResolve override func field( typeProvider: TypeProvider, coders: Coders ) throws -> (String, GraphQLField) { + let resolve = self.resolve + let subscribe = self.subscribe let field = try GraphQLField( type: typeProvider.getOutputType(from: FieldType.self, field: name), description: description, deprecationReason: deprecationReason, args: arguments(typeProvider: typeProvider, coders: coders), - resolve: { source, arguments, context, eventLoopGroup, _ in + resolve: { source, arguments, context, _ in guard let _source = source as? SourceEventType else { throw GraphQLError( message: "Expected source type \(SourceEventType.self) but got \(type(of: source))" @@ -38,9 +40,9 @@ public class SubscriptionField< } let args = try coders.decoder.decode(Arguments.self, from: arguments) - return try self.resolve(_source)(_context, args, eventLoopGroup) + return try await resolve(_source)(_context, args) }, - subscribe: { source, arguments, context, eventLoopGroup, _ in + subscribe: { source, arguments, context, _ in guard let _source = source as? ObjectType else { throw GraphQLError( message: "Expected source type \(ObjectType.self) but got \(type(of: source))" @@ -54,8 +56,7 @@ public class SubscriptionField< } let args = try coders.decoder.decode(Arguments.self, from: arguments) - return try self.subscribe(_source)(_context, args, eventLoopGroup) - .map { $0.map { $0 as Any } } + return try await subscribe(_source)(_context, args) } ) @@ -76,12 +77,12 @@ public class SubscriptionField< init( name: String, arguments: [ArgumentComponent], - resolve: @escaping AsyncResolve, + resolve: @escaping AsyncResolve, subscribe: @escaping AsyncResolve< ObjectType, Context, Arguments, - EventStream + SubSequence > ) { self.name = name @@ -90,7 +91,7 @@ public class SubscriptionField< self.subscribe = subscribe } - convenience init( + convenience init( name: String, arguments: [ArgumentComponent], asyncResolve: @escaping AsyncResolve, @@ -98,12 +99,12 @@ public class SubscriptionField< ObjectType, Context, Arguments, - EventStream + SubSequence > ) { - let resolve: AsyncResolve = { type in - { context, arguments, eventLoopGroup in - try asyncResolve(type)(context, arguments, eventLoopGroup).map { $0 } + let resolve: AsyncResolve = { type in + { context, arguments in + try await asyncResolve(type)(context, arguments) } } self.init(name: name, arguments: arguments, resolve: resolve, subscribe: asyncSubscribe) @@ -117,88 +118,23 @@ public class SubscriptionField< ObjectType, Context, Arguments, - EventStream + SubSequence > ) { - let resolve: AsyncResolve = { source in - { _, _, eventLoopGroup in - eventLoopGroup.next().makeSucceededFuture(source) - } - } - self.init(name: name, arguments: arguments, resolve: resolve, subscribe: asyncSubscribe) - } - - convenience init( - name: String, - arguments: [ArgumentComponent], - simpleAsyncResolve: @escaping SimpleAsyncResolve< + let resolve: AsyncResolve< SourceEventType, Context, Arguments, - ResolveType - >, - simpleAsyncSubscribe: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - > - ) { - let asyncResolve: AsyncResolve = { type in - { context, arguments, group in - // We hop to guarantee that the future will - // return in the same event loop group of the execution. - try simpleAsyncResolve(type)(context, arguments).hop(to: group.next()) + (any Sendable)? + > = { source in + { _, _ in + source } } - - let asyncSubscribe: AsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - > = { type in - { context, arguments, group in - // We hop to guarantee that the future will - // return in the same event loop group of the execution. - try simpleAsyncSubscribe(type)(context, arguments).hop(to: group.next()) - } - } - self.init( - name: name, - arguments: arguments, - asyncResolve: asyncResolve, - asyncSubscribe: asyncSubscribe - ) - } - - convenience init( - name: String, - arguments: [ArgumentComponent], - as: FieldType.Type, - simpleAsyncSubscribe: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - > - ) { - let asyncSubscribe: AsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - > = { type in - { context, arguments, group in - // We hop to guarantee that the future will - // return in the same event loop group of the execution. - try simpleAsyncSubscribe(type)(context, arguments).hop(to: group.next()) - } - } - self.init(name: name, arguments: arguments, as: `as`, asyncSubscribe: asyncSubscribe) + self.init(name: name, arguments: arguments, resolve: resolve, subscribe: asyncSubscribe) } - convenience init( + convenience init( name: String, arguments: [ArgumentComponent], syncResolve: @escaping SyncResolve, @@ -206,13 +142,12 @@ public class SubscriptionField< ObjectType, Context, Arguments, - EventStream + SubSequence > ) { let asyncResolve: AsyncResolve = { type in - { context, arguments, group in - let result = try syncResolve(type)(context, arguments) - return group.next().makeSucceededFuture(result) + { context, arguments in + try syncResolve(type)(context, arguments) } } @@ -220,11 +155,10 @@ public class SubscriptionField< ObjectType, Context, Arguments, - EventStream + SubSequence > = { type in - { context, arguments, group in - let result = try syncSubscribe(type)(context, arguments) - return group.next().makeSucceededFuture(result) + { context, arguments in + try syncSubscribe(type)(context, arguments) } } self.init( @@ -243,18 +177,17 @@ public class SubscriptionField< ObjectType, Context, Arguments, - EventStream + SubSequence > ) { let asyncSubscribe: AsyncResolve< ObjectType, Context, Arguments, - EventStream + SubSequence > = { type in - { context, arguments, group in - let result = try syncSubscribe(type)(context, arguments) - return group.next().makeSucceededFuture(result) + { context, arguments in + try syncSubscribe(type)(context, arguments) } } self.init(name: name, arguments: arguments, as: `as`, asyncSubscribe: asyncSubscribe) @@ -271,7 +204,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) { @@ -290,7 +223,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -312,7 +245,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) { @@ -326,7 +259,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -334,14 +267,14 @@ public extension SubscriptionField { self.init(name: name, arguments: arguments(), as: `as`, asyncSubscribe: subFunc) } - convenience init( + convenience init( _ name: String, at function: @escaping AsyncResolve, atSub subFunc: @escaping AsyncResolve< ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) { @@ -353,14 +286,14 @@ public extension SubscriptionField { ) } - convenience init( + convenience init( _ name: String, at function: @escaping AsyncResolve, atSub subFunc: @escaping AsyncResolve< ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -374,128 +307,7 @@ public extension SubscriptionField { } } -// MARK: SimpleAsyncResolve Initializers - -public extension SubscriptionField { - convenience init( - _ name: String, - at function: @escaping SimpleAsyncResolve, - atSub subFunc: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init( - name: name, - arguments: [argument()], - simpleAsyncResolve: function, - simpleAsyncSubscribe: subFunc - ) - } - - convenience init( - _ name: String, - at function: @escaping SimpleAsyncResolve, - atSub subFunc: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init( - name: name, - arguments: arguments(), - simpleAsyncResolve: function, - simpleAsyncSubscribe: subFunc - ) - } -} - public extension SubscriptionField { - convenience init( - _ name: String, - as: FieldType.Type, - atSub subFunc: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init(name: name, arguments: [argument()], as: `as`, simpleAsyncSubscribe: subFunc) - } - - convenience init( - _ name: String, - as: FieldType.Type, - atSub subFunc: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init(name: name, arguments: arguments(), as: `as`, simpleAsyncSubscribe: subFunc) - } - - convenience init( - _ name: String, - at function: @escaping SimpleAsyncResolve, - as _: FieldType.Type, - atSub subFunc: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init( - name: name, - arguments: [argument()], - simpleAsyncResolve: function, - simpleAsyncSubscribe: subFunc - ) - } - - convenience init( - _ name: String, - at function: @escaping SimpleAsyncResolve, - as _: FieldType.Type, - atSub subFunc: @escaping SimpleAsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init( - name: name, - arguments: arguments(), - simpleAsyncResolve: function, - simpleAsyncSubscribe: subFunc - ) - } -} - -// MARK: SyncResolve Initializers - -// '@_disfavoredOverload' is included below because otherwise `SimpleAsyncResolve` initializers also match this signature, causing the -// calls to be ambiguous. We prefer that if an EventLoopFuture is returned from the resolve, that `SimpleAsyncResolve` is matched. - -public extension SubscriptionField { - @_disfavoredOverload convenience init( _ name: String, at function: @escaping SyncResolve, @@ -503,7 +315,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) { @@ -515,7 +327,6 @@ public extension SubscriptionField { ) } - @_disfavoredOverload convenience init( _ name: String, at function: @escaping SyncResolve, @@ -523,7 +334,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -541,7 +352,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) { @@ -556,7 +367,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -565,7 +376,7 @@ public extension SubscriptionField { } @_disfavoredOverload - convenience init( + convenience init( _ name: String, at function: @escaping SyncResolve, as _: FieldType.Type, @@ -573,7 +384,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ argument: () -> ArgumentComponent ) { @@ -586,7 +397,7 @@ public extension SubscriptionField { } @_disfavoredOverload - convenience init( + convenience init( _ name: String, at function: @escaping SyncResolve, as _: FieldType.Type, @@ -594,7 +405,7 @@ public extension SubscriptionField { ObjectType, Context, Arguments, - EventStream + SubSequence >, @ArgumentComponentBuilder _ arguments: () -> [ArgumentComponent] = { [] } @@ -603,228 +414,6 @@ public extension SubscriptionField { } } -public extension SubscriptionField { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - name: String, - arguments: [ArgumentComponent], - concurrentResolve: @escaping ConcurrentResolve< - SourceEventType, - Context, - Arguments, - ResolveType - >, - concurrentSubscribe: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - > - ) { - let asyncResolve: AsyncResolve = { type in - { context, arguments, eventLoopGroup in - let promise = eventLoopGroup.next().makePromise(of: ResolveType.self) - promise.completeWithTask { - try await concurrentResolve(type)(context, arguments) - } - return promise.futureResult - } - } - let asyncSubscribe: AsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - > = { type in - { context, arguments, eventLoopGroup in - let promise = eventLoopGroup.next() - .makePromise(of: EventStream.self) - promise.completeWithTask { - try await concurrentSubscribe(type)(context, arguments) - } - return promise.futureResult - } - } - self.init( - name: name, - arguments: arguments, - asyncResolve: asyncResolve, - asyncSubscribe: asyncSubscribe - ) - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - name: String, - arguments: [ArgumentComponent], - as: FieldType.Type, - concurrentSubscribe: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - > - ) { - let asyncSubscribe: AsyncResolve< - ObjectType, - Context, - Arguments, - EventStream - > = { type in - { context, arguments, eventLoopGroup in - let promise = eventLoopGroup.next() - .makePromise(of: EventStream.self) - promise.completeWithTask { - try await concurrentSubscribe(type)(context, arguments) - } - return promise.futureResult - } - } - self.init(name: name, arguments: arguments, as: `as`, asyncSubscribe: asyncSubscribe) - } -} - -// MARK: ConcurrentResolve Initializers - -public extension SubscriptionField { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - at function: @escaping ConcurrentResolve< - SourceEventType, - Context, - Arguments, - FieldType - >, - atSub subFunc: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init( - name: name, - arguments: [argument()], - concurrentResolve: function, - concurrentSubscribe: subFunc - ) - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - at function: @escaping ConcurrentResolve< - SourceEventType, - Context, - Arguments, - FieldType - >, - atSub subFunc: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init( - name: name, - arguments: arguments(), - concurrentResolve: function, - concurrentSubscribe: subFunc - ) - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - as: FieldType.Type, - atSub subFunc: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () -> ArgumentComponent - ) { - self.init(name: name, arguments: [arguments()], as: `as`, concurrentSubscribe: subFunc) - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - as: FieldType.Type, - atSub subFunc: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init(name: name, arguments: arguments(), as: `as`, concurrentSubscribe: subFunc) - } -} - -public extension SubscriptionField { - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - at function: @escaping ConcurrentResolve< - SourceEventType, - Context, - Arguments, - ResolveType - >, - as _: FieldType.Type, - atSub subFunc: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ argument: () -> ArgumentComponent - ) { - self.init( - name: name, - arguments: [argument()], - concurrentResolve: function, - concurrentSubscribe: subFunc - ) - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - convenience init( - _ name: String, - at function: @escaping ConcurrentResolve< - SourceEventType, - Context, - Arguments, - ResolveType - >, - as _: FieldType.Type, - atSub subFunc: @escaping ConcurrentResolve< - ObjectType, - Context, - Arguments, - EventStream - >, - @ArgumentComponentBuilder _ arguments: () - -> [ArgumentComponent] = { [] } - ) { - self.init( - name: name, - arguments: arguments(), - concurrentResolve: function, - concurrentSubscribe: subFunc - ) - } -} - // TODO: Determine if we can use keypaths to initialize // MARK: Keypath Initializers diff --git a/Sources/Graphiti/Subscription/SubscribeResolve.swift b/Sources/Graphiti/Subscription/SubscribeResolve.swift index 65fdc962..9e55582f 100644 --- a/Sources/Graphiti/Subscription/SubscribeResolve.swift +++ b/Sources/Graphiti/Subscription/SubscribeResolve.swift @@ -1,9 +1,7 @@ -import NIO public typealias SubscribeResolve = ( _ object: ObjectType ) -> ( _ context: Context, - _ arguments: Arguments, - _ eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture + _ arguments: Arguments +) async throws -> ResolveType diff --git a/Sources/Graphiti/Subscription/Subscription.swift b/Sources/Graphiti/Subscription/Subscription.swift index e7752054..cf50092f 100644 --- a/Sources/Graphiti/Subscription/Subscription.swift +++ b/Sources/Graphiti/Subscription/Subscription.swift @@ -1,9 +1,15 @@ import GraphQL -public final class Subscription: Component { +public final class Subscription< + Resolver: Sendable, + Context: Sendable +>: Component< + Resolver, + Context +> { let fields: [FieldComponent] - let isTypeOf: GraphQLIsTypeOf = { source, _, _ in + let isTypeOf: GraphQLIsTypeOf = { source, _ in source is Resolver } diff --git a/Sources/Graphiti/Type/Type.swift b/Sources/Graphiti/Type/Type.swift index 96c74bdc..de98e9c4 100644 --- a/Sources/Graphiti/Type/Type.swift +++ b/Sources/Graphiti/Type/Type.swift @@ -1,6 +1,10 @@ import GraphQL -public final class Type: TypeComponent< +public final class Type< + Resolver: Sendable, + Context: Sendable, + ObjectType: Sendable +>: TypeComponent< Resolver, Context > { @@ -8,7 +12,7 @@ public final class Type: TypeComponent< var keys: [KeyComponent] let fields: [FieldComponent] - let isTypeOf: GraphQLIsTypeOf = { source, _, _ in + let isTypeOf: GraphQLIsTypeOf = { source, _ in source is ObjectType } @@ -36,7 +40,8 @@ public final class Type: TypeComponent< // If federation keys are included, create resolver closure if !keys.isEmpty { - let resolve: GraphQLFieldResolve = { source, args, context, eventLoopGroup, _ in + let keys = self.keys + let resolve: GraphQLFieldResolve = { source, args, context, _ in guard let s = source as? Resolver else { throw GraphQLError( message: "Expected source type \(ObjectType.self) but got \(type(of: source))" @@ -49,7 +54,7 @@ public final class Type: TypeComponent< ) } - let keyMatch = self.keys.first { key in + let keyMatch = keys.first { key in key.mapMatchesArguments(args, coders: coders) } guard let key = keyMatch else { @@ -58,11 +63,10 @@ public final class Type: TypeComponent< ) } - return try key.resolveMap( + return try await key.resolveMap( resolver: s, context: c, map: args, - eventLoopGroup: eventLoopGroup, coders: coders ) } diff --git a/Sources/Graphiti/Union/Union.swift b/Sources/Graphiti/Union/Union.swift index 791d4957..ea2310a0 100644 --- a/Sources/Graphiti/Union/Union.swift +++ b/Sources/Graphiti/Union/Union.swift @@ -1,6 +1,13 @@ import GraphQL -public final class Union: TypeComponent { +public final class Union< + Resolver: Sendable, + Context: Sendable, + UnionType +>: TypeComponent< + Resolver, + Context +> { private let members: [Any.Type] override func update(typeProvider: SchemaTypeProvider, coders _: Coders) throws { diff --git a/Sources/Graphiti/Validation/NoIntrospectionRule.swift b/Sources/Graphiti/Validation/NoIntrospectionRule.swift index 784802b8..c25a40b0 100644 --- a/Sources/Graphiti/Validation/NoIntrospectionRule.swift +++ b/Sources/Graphiti/Validation/NoIntrospectionRule.swift @@ -1,5 +1,6 @@ import GraphQL +@Sendable public func NoIntrospectionRule(context: ValidationContext) -> Visitor { return Visitor(enter: { node, _, _, _, _ in if let field = node as? GraphQL.Field, ["__schema", "__type"].contains(field.name.value) { diff --git a/Tests/GraphitiTests/ConnectionTests.swift b/Tests/GraphitiTests/ConnectionTests.swift index 96e3f43c..bba44987 100644 --- a/Tests/GraphitiTests/ConnectionTests.swift +++ b/Tests/GraphitiTests/ConnectionTests.swift @@ -1,6 +1,5 @@ import Foundation import Graphiti -import NIO import XCTest class ConnectionTests: XCTestCase { @@ -40,35 +39,33 @@ class ConnectionTests: XCTestCase { } } - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - /// Test that connection objects work as expected - func testConnection() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments { - edges { - cursor - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testConnection() async throws { + let result = try await schema.execute( + request: """ + { + comments { + edges { + cursor + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -108,31 +105,31 @@ class ConnectionTests: XCTestCase { } /// Test that `first` argument works as intended - func testFirst() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(first: 1) { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testFirst() async throws { + let result = try await schema.execute( + request: """ + { + comments(first: 1) { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -157,31 +154,31 @@ class ConnectionTests: XCTestCase { } /// Test that `after` argument works as intended - func testAfter() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(after: "MQ==") { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testAfter() async throws { + let result = try await schema.execute( + request: """ + { + comments(after: "MQ==") { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -212,31 +209,31 @@ class ConnectionTests: XCTestCase { } /// Test that mixing `first` and `after` arguments works as intended - func testFirstAfter() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(first: 1, after: "MQ==") { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testFirstAfter() async throws { + let result = try await schema.execute( + request: """ + { + comments(first: 1, after: "MQ==") { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -261,31 +258,31 @@ class ConnectionTests: XCTestCase { } /// Test that `last` argument works as intended - func testLast() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(last: 1) { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testLast() async throws { + let result = try await schema.execute( + request: """ + { + comments(last: 1) { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -310,31 +307,31 @@ class ConnectionTests: XCTestCase { } /// Test that `before` argument works as intended - func testBefore() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(before: "Mw==") { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testBefore() async throws { + let result = try await schema.execute( + request: """ + { + comments(before: "Mw==") { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -365,31 +362,31 @@ class ConnectionTests: XCTestCase { } /// Test that mixing `last` with `before` argument works as intended - func testLastBefore() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(last: 1, before: "Mw==") { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testLastBefore() async throws { + let result = try await schema.execute( + request: """ + { + comments(last: 1, before: "Mw==") { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -414,31 +411,31 @@ class ConnectionTests: XCTestCase { } /// Test that mixing `after` with `before` argument works as intended - func testAfterBefore() throws { - XCTAssertEqual( - try schema.execute( - request: """ - { - comments(after: "MQ==", before: "Mw==") { - edges { - node { - id - message - } - } - pageInfo { - hasPreviousPage - hasNextPage - startCursor - endCursor + func testAfterBefore() async throws { + let result = try await schema.execute( + request: """ + { + comments(after: "MQ==", before: "Mw==") { + edges { + node { + id + message } } + pageInfo { + hasPreviousPage + hasNextPage + startCursor + endCursor + } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "comments": [ @@ -463,7 +460,7 @@ class ConnectionTests: XCTestCase { } /// Test that adjusting names using `as` works - func testNaming() throws { + func testNaming() async throws { struct ChatObject: Codable { func messages( context _: NoContext, @@ -509,26 +506,26 @@ class ConnectionTests: XCTestCase { } } - XCTAssertEqual( - try schema.execute( - request: """ - { - chatObject { - messages { - edges { - node { - id - text - } + let result = try await schema.execute( + request: """ + { + chatObject { + messages { + edges { + node { + id + text } } } } - """, - resolver: .init(), - context: NoContext(), - eventLoopGroup: eventLoopGroup - ).wait(), + } + """, + resolver: .init(), + context: NoContext() + ) + XCTAssertEqual( + result, .init( data: [ "chatObject": [ diff --git a/Tests/GraphitiTests/DefaultValueTests.swift b/Tests/GraphitiTests/DefaultValueTests.swift index f5b41a8e..81f11c54 100644 --- a/Tests/GraphitiTests/DefaultValueTests.swift +++ b/Tests/GraphitiTests/DefaultValueTests.swift @@ -1,120 +1,117 @@ import Graphiti import GraphQL -import NIO import XCTest class DefaultValueTests: XCTestCase { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - func testBoolDefault() async throws { + let result = try await DefaultValueAPI().execute( + request: """ + { + bool + } + """, + context: NoContext() + ) XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - bool - } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + result, .init(data: ["bool": true]) ) } func testIntDefault() async throws { + let result = try await DefaultValueAPI().execute( + request: """ + { + int + } + """, + context: NoContext() + ) XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - int - } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + result, .init(data: ["int": 1]) ) } func testFloatDefault() async throws { + let result = try await DefaultValueAPI().execute( + request: """ + { + float + } + """, + context: NoContext() + ) XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - float - } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + result, .init(data: ["float": 1.1]) ) } func testStringDefault() async throws { + let result = try await DefaultValueAPI().execute( + request: """ + { + string + } + """, + context: NoContext() + ) XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - string - } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + result, .init(data: ["string": "hello"]) ) } func testEnumDefault() async throws { + let result = try await DefaultValueAPI().execute( + request: """ + { + enum + } + """, + context: NoContext() + ) XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - enum - } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + result, .init(data: ["enum": "valueA"]) ) } func testArrayDefault() async throws { + let result = try await DefaultValueAPI().execute( + request: """ + { + array + } + """, + context: NoContext() + ) XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - array - } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + result, .init(data: ["array": ["a", "b", "c"]]) ) } func testInputDefault() async throws { // Test input object argument default - XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - input { - bool - int - float - string - enum - array - } + var result = try await DefaultValueAPI().execute( + request: """ + { + input { + bool + int + float + string + enum + array } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, .init(data: [ "input": [ "bool": true, @@ -128,23 +125,23 @@ class DefaultValueTests: XCTestCase { ) // Test input object field defaults - XCTAssertEqual( - try DefaultValueAPI().execute( - request: """ - { - input(input: {bool: true}) { - bool - int - float - string - enum - array - } + result = try await DefaultValueAPI().execute( + request: """ + { + input(input: {bool: true}) { + bool + int + float + string + enum + array } - """, - context: NoContext(), - on: eventLoopGroup - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, .init(data: [ "input": [ "bool": true, diff --git a/Tests/GraphitiTests/DirectiveTests/DirectiveTests.swift b/Tests/GraphitiTests/DirectiveTests/DirectiveTests.swift index 34db5c8f..5df7d389 100644 --- a/Tests/GraphitiTests/DirectiveTests/DirectiveTests.swift +++ b/Tests/GraphitiTests/DirectiveTests/DirectiveTests.swift @@ -1,17 +1,11 @@ @testable import Graphiti import GraphQL -import NIO import XCTest class DirectiveTests: XCTestCase { private let api = StarWarsAPI() - private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - deinit { - try? self.group.syncShutdownGracefully() - } - - func testSkip() throws { + func testSkip() async throws { let query = """ query FetchHeroNameWithSkip($skipName: Boolean!) { hero { @@ -25,12 +19,11 @@ class DirectiveTests: XCTestCase { "skipName": true, ] - let response = try api.execute( + let response = try await api.execute( request: query, context: StarWarsContext(), - on: group, variables: input - ).wait() + ) let expected = GraphQLResult( data: [ @@ -43,7 +36,7 @@ class DirectiveTests: XCTestCase { XCTAssertEqual(response, expected) } - func testInclude() throws { + func testInclude() async throws { let query = """ query FetchHeroNameWithSkip($includeName: Boolean!) { hero { @@ -57,12 +50,11 @@ class DirectiveTests: XCTestCase { "includeName": false, ] - let response = try api.execute( + let response = try await api.execute( request: query, context: StarWarsContext(), - on: group, variables: input - ).wait() + ) let expected = GraphQLResult( data: [ @@ -75,20 +67,20 @@ class DirectiveTests: XCTestCase { XCTAssertEqual(response, expected) } - func testOneOfAcceptsGoodValue() throws { - try XCTAssertEqual( - OneOfAPI().execute( - request: """ - query { - test(input: {a: "abc"}) { - a - b - } + func testOneOfAcceptsGoodValue() async throws { + let result = try await OneOfAPI().execute( + request: """ + query { + test(input: {a: "abc"}) { + a + b } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "test": [ @@ -100,20 +92,20 @@ class DirectiveTests: XCTestCase { ) } - func testOneOfRejectsBadValue() throws { - try XCTAssertEqual( - OneOfAPI().execute( - request: """ - query { - test(input: {a: "abc", b: 123}) { - a - b - } + func testOneOfRejectsBadValue() async throws { + let result = try await OneOfAPI().execute( + request: """ + query { + test(input: {a: "abc", b: 123}) { + a + b } - """, - context: NoContext(), - on: group - ).wait().errors[0].message, + } + """, + context: NoContext() + ) + XCTAssertEqual( + result.errors[0].message, #"OneOf Input Object "TestInputObject" must specify exactly one key."# ) } diff --git a/Tests/GraphitiTests/FederationTests/FederationOnlySchemaTests.swift b/Tests/GraphitiTests/FederationTests/FederationOnlySchemaTests.swift index 9092c8d5..8eeec7fd 100644 --- a/Tests/GraphitiTests/FederationTests/FederationOnlySchemaTests.swift +++ b/Tests/GraphitiTests/FederationTests/FederationOnlySchemaTests.swift @@ -1,11 +1,9 @@ import Foundation import Graphiti import GraphQL -import NIO import XCTest final class FederationOnlySchemaTests: XCTestCase { - private var group: MultiThreadedEventLoopGroup! private var api: FederationOnlyAPI! struct Profile: Codable { @@ -71,28 +69,22 @@ final class FederationOnlySchemaTests: XCTestCase { } } .build() - group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) api = FederationOnlyAPI(resolver: FederationOnlyResolver(), schema: schema) } override func tearDownWithError() throws { - try group.syncShutdownGracefully() - group = nil api = nil } - func execute(request: String, variables: [String: Map] = [:]) throws -> GraphQLResult { - try api - .execute( - request: request, - context: NoContext(), - on: group, - variables: variables - ) - .wait() + func execute(request: String, variables: [String: Map] = [:]) async throws -> GraphQLResult { + try await api.execute( + request: request, + context: NoContext(), + variables: variables + ) } - func testUserFederationSimple() throws { + func testUserFederationSimple() async throws { let representations: [String: Map] = [ "representations": [ ["__typename": "User", "id": "1234"], @@ -107,11 +99,12 @@ final class FederationOnlySchemaTests: XCTestCase { id } } - } + } """ - try XCTAssertEqual( - execute(request: query, variables: representations), + let result = try await execute(request: query, variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ @@ -122,7 +115,7 @@ final class FederationOnlySchemaTests: XCTestCase { ) } - func testUserFederationNested() throws { + func testUserFederationNested() async throws { let representations: [String: Map] = [ "representations": [ ["__typename": "User", "id": "1234"], @@ -138,11 +131,12 @@ final class FederationOnlySchemaTests: XCTestCase { profile { name, email } } } - } + } """ - try XCTAssertEqual( - execute(request: query, variables: representations), + let result = try await execute(request: query, variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ @@ -157,7 +151,7 @@ final class FederationOnlySchemaTests: XCTestCase { ) } - func testUserFederationNestedOptional() throws { + func testUserFederationNestedOptional() async throws { let representations: [String: Map] = [ "representations": [ ["__typename": "User", "id": "1"], @@ -173,11 +167,12 @@ final class FederationOnlySchemaTests: XCTestCase { profile { name, email } } } - } + } """ - try XCTAssertEqual( - execute(request: query, variables: representations), + let result = try await execute(request: query, variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ diff --git a/Tests/GraphitiTests/FederationTests/FederationTests.swift b/Tests/GraphitiTests/FederationTests/FederationTests.swift index c7cde52d..14f5620c 100644 --- a/Tests/GraphitiTests/FederationTests/FederationTests.swift +++ b/Tests/GraphitiTests/FederationTests/FederationTests.swift @@ -1,11 +1,9 @@ import Foundation import Graphiti import GraphQL -import NIO import XCTest final class FederationTests: XCTestCase { - private var group: MultiThreadedEventLoopGroup! private var api: ProductAPI! override func setUpWithError() throws { @@ -13,35 +11,37 @@ final class FederationTests: XCTestCase { .use(partials: [ProductSchema()]) .setFederatedSDL(to: loadSDL()) .build() - group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) api = try ProductAPI(resolver: ProductResolver(sdl: loadSDL()), schema: schema) } override func tearDownWithError() throws { - try group.syncShutdownGracefully() - group = nil api = nil } // Test Queries from https://github.com/apollographql/apollo-federation-subgraph-compatibility/blob/main/COMPATIBILITY.md - func testServiceQuery() throws { - try XCTAssertEqual(execute(request: query("service")), GraphQLResult(data: [ - "_service": [ - "sdl": Map(stringLiteral: loadSDL()), - ], - ])) + func testServiceQuery() async throws { + let result = try await execute(request: query("service")) + try XCTAssertEqual( + result, + GraphQLResult(data: [ + "_service": [ + "sdl": Map(stringLiteral: loadSDL()), + ], + ]) + ) } - func testEntityKey() throws { + func testEntityKey() async throws { let representations: [String: Map] = [ "representations": [ ["__typename": "User", "email": "support@apollographql.com"], ], ] - try XCTAssertEqual( - execute(request: query("entities"), variables: representations), + let result = try await execute(request: query("entities"), variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ @@ -56,7 +56,7 @@ final class FederationTests: XCTestCase { ) } - func testEntityMultipleKey() throws { + func testEntityMultipleKey() async throws { let representations: [String: Map] = [ "representations": [ [ @@ -67,8 +67,9 @@ final class FederationTests: XCTestCase { ], ] - try XCTAssertEqual( - execute(request: query("entities"), variables: representations), + let result = try await execute(request: query("entities"), variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ @@ -88,15 +89,16 @@ final class FederationTests: XCTestCase { ) } - func testEntityCompositeKey() throws { + func testEntityCompositeKey() async throws { let representations: [String: Map] = [ "representations": [ ["__typename": "ProductResearch", "study": ["caseNumber": "1234"]], ], ] - try XCTAssertEqual( - execute(request: query("entities"), variables: representations), + let result = try await execute(request: query("entities"), variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ @@ -111,7 +113,7 @@ final class FederationTests: XCTestCase { ) } - func testEntityMultipleKeys() throws { + func testEntityMultipleKeys() async throws { let representations: [String: Map] = [ "representations": [ ["__typename": "Product", "id": "apollo-federation"], @@ -120,8 +122,9 @@ final class FederationTests: XCTestCase { ], ] - try XCTAssertEqual( - execute(request: query("entities"), variables: representations), + let result = try await execute(request: query("entities"), variables: representations) + XCTAssertEqual( + result, GraphQLResult(data: [ "_entities": [ [ @@ -253,9 +256,7 @@ extension FederationTests { return try String(contentsOf: url) } - func execute(request: String, variables: [String: Map] = [:]) throws -> GraphQLResult { - try api - .execute(request: request, context: ProductContext(), on: group, variables: variables) - .wait() + func execute(request: String, variables: [String: Map] = [:]) async throws -> GraphQLResult { + try await api.execute(request: request, context: ProductContext(), variables: variables) } } diff --git a/Tests/GraphitiTests/HelloWorldTests/HelloWorldAsyncTests.swift b/Tests/GraphitiTests/HelloWorldTests/HelloWorldAsyncTests.swift index 186d4984..20da932e 100644 --- a/Tests/GraphitiTests/HelloWorldTests/HelloWorldAsyncTests.swift +++ b/Tests/GraphitiTests/HelloWorldTests/HelloWorldAsyncTests.swift @@ -1,6 +1,5 @@ @testable import Graphiti import GraphQL -import NIO import XCTest @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) @@ -17,24 +16,26 @@ extension HelloResolver { }.value } - func subscribeUser(context _: HelloContext, arguments _: NoArguments) -> EventStream { - pubsub.subscribe() + func subscribeUser( + context _: HelloContext, + arguments _: NoArguments + ) async -> AsyncThrowingStream { + await pubsub.subscribe() } func futureSubscribeUser( context _: HelloContext, - arguments _: NoArguments, - group: EventLoopGroup - ) -> EventLoopFuture> { - group.next().makeSucceededFuture(pubsub.subscribe()) + arguments _: NoArguments + ) async -> AsyncThrowingStream { + await pubsub.subscribe() } func asyncSubscribeUser( context _: HelloContext, arguments _: NoArguments - ) async -> EventStream { + ) async -> AsyncThrowingStream { return await Task { - pubsub.subscribe() + await pubsub.subscribe() }.value } } @@ -123,15 +124,13 @@ struct HelloAsyncAPI: API { @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) class HelloWorldAsyncTests: XCTestCase { private let api = HelloAsyncAPI() - private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) /// Tests that async version of API.execute works as expected func testAsyncExecute() async throws { let query = "{ hello }" let result = try await api.execute( request: query, - context: api.context, - on: group + context: api.context ) XCTAssertEqual( result, @@ -144,8 +143,7 @@ class HelloWorldAsyncTests: XCTestCase { let query = "{ asyncHello }" let result = try await api.execute( request: query, - context: api.context, - on: group + context: api.context ) XCTAssertEqual( result, @@ -164,24 +162,15 @@ class HelloWorldAsyncTests: XCTestCase { } """ - let subscriptionResult = try await api.subscribe( + let subscription = try await api.subscribe( request: request, - context: api.context, - on: group - ) - guard let subscription = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + context: api.context + ).get() + var iterator = subscription.makeAsyncIterator() - pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) + await pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult(data: [ @@ -206,24 +195,15 @@ class HelloWorldAsyncTests: XCTestCase { } """ - let subscriptionResult = try await api.subscribe( + let subscription = try await api.subscribe( request: request, - context: api.context, - on: group - ) - guard let subscription = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + context: api.context + ).get() + var iterator = subscription.makeAsyncIterator() - pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) + await pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult(data: [ @@ -248,24 +228,15 @@ class HelloWorldAsyncTests: XCTestCase { } """ - let subscriptionResult = try await api.subscribe( + let subscription = try await api.subscribe( request: request, - context: api.context, - on: group - ) - guard let subscription = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + context: api.context + ).get() + var iterator = subscription.makeAsyncIterator() - pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) + await pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult(data: [ @@ -288,24 +259,15 @@ class HelloWorldAsyncTests: XCTestCase { } """ - let subscriptionResult = try await api.subscribe( + let subscription = try await api.subscribe( request: request, - context: api.context, - on: group - ) - guard let subscription = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + context: api.context + ).get() + var iterator = subscription.makeAsyncIterator() - pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) + await pubsub.publish(event: User(id: "124", name: "Jerry", friends: nil)) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult(data: [ @@ -320,7 +282,7 @@ class HelloWorldAsyncTests: XCTestCase { /// A very simple publish/subscriber used for testing @available(macOS 12, iOS 15, watchOS 8, tvOS 15, *) -class SimplePubSub { +actor SimplePubSub: Sendable { private var subscribers: [Subscriber] init() { @@ -339,8 +301,8 @@ class SimplePubSub { } } - func subscribe() -> ConcurrentEventStream { - let asyncStream = AsyncThrowingStream { continuation in + func subscribe() -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in let subscriber = Subscriber( callback: { newValue in continuation.yield(newValue) @@ -351,11 +313,10 @@ class SimplePubSub { ) subscribers.append(subscriber) } - return ConcurrentEventStream(asyncStream) } } -struct Subscriber { +struct Subscriber { let callback: (T) -> Void let cancel: () -> Void } diff --git a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift index eb316bc0..70374184 100644 --- a/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphitiTests/HelloWorldTests/HelloWorldTests.swift @@ -1,6 +1,5 @@ @testable import Graphiti import GraphQL -import NIO import XCTest struct ID: Codable { @@ -57,23 +56,22 @@ struct UserEvent { let user: User } -final class HelloContext { +final class HelloContext: Sendable { func hello() -> String { "world" } } -struct HelloResolver { +struct HelloResolver: Sendable { func hello(context: HelloContext, arguments _: NoArguments) -> String { context.hello() } func futureHello( context: HelloContext, - arguments _: NoArguments, - group: EventLoopGroup - ) -> EventLoopFuture { - group.next().makeSucceededFuture(context.hello()) + arguments _: NoArguments + ) -> String { + context.hello() } struct FloatArguments: Codable { @@ -159,41 +157,36 @@ struct HelloAPI: API { class HelloWorldTests: XCTestCase { private let api = HelloAPI() - private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - deinit { - try? self.group.syncShutdownGracefully() - } - func testHello() throws { + func testHello() async throws { + let result = try await api.execute( + request: "{ hello }", + context: api.context + ) XCTAssertEqual( - try api.execute( - request: "{ hello }", - context: api.context, - on: group - ).wait(), + result, GraphQLResult(data: ["hello": "world"]) ) } - func testFutureHello() throws { + func testFutureHello() async throws { + let result = try await api.execute( + request: "{ futureHello }", + context: api.context + ) XCTAssertEqual( - try api.execute( - request: "{ futureHello }", - context: api.context, - on: group - ).wait(), + result, GraphQLResult(data: ["futureHello": "world"]) ) } - func testBoyhowdy() throws { + func testBoyhowdy() async throws { + let result = try await api.execute( + request: "{ boyhowdy }", + context: api.context + ) XCTAssertEqual( - try api.execute( - request: "{ boyhowdy }", - context: api.context, - on: group - ).wait(), + result, GraphQLResult( errors: [ GraphQLError( @@ -205,66 +198,87 @@ class HelloWorldTests: XCTestCase { ) } - func testScalar() throws { + func testScalar() async throws { + var result = try await api.execute( + request: """ + query Query($float: Float!) { + float(float: $float) + } + """, + context: api.context, + variables: ["float": 4] + ) XCTAssertEqual( - try api.execute( - request: """ - query Query($float: Float!) { - float(float: $float) - } - """, - context: api.context, - on: group, - variables: ["float": 4] - ).wait(), + result, GraphQLResult(data: ["float": 4.0]) ) + result = try await api.execute( + request: """ + query Query { + float(float: 4) + } + """, + context: api.context + ) XCTAssertEqual( - try api.execute( - request: """ - query Query { - float(float: 4) - } - """, - context: api.context, - on: group - ).wait(), + result, GraphQLResult(data: ["float": 4.0]) ) + result = try await api.execute( + request: """ + query Query($id: ID!) { + id(id: $id) + } + """, + context: api.context, + variables: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"] + ) XCTAssertEqual( - try api.execute( - request: """ - query Query($id: ID!) { - id(id: $id) - } - """, - context: api.context, - on: group, - variables: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"] - ).wait(), + result, GraphQLResult(data: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"]) ) + result = try await api.execute( + request: """ + query Query { + id(id: "85b8d502-8190-40ab-b18f-88edd297d8b6") + } + """, + context: api.context + ) XCTAssertEqual( - try api.execute( - request: """ - query Query { - id(id: "85b8d502-8190-40ab-b18f-88edd297d8b6") - } - """, - context: api.context, - on: group - ).wait(), + result, GraphQLResult(data: ["id": "85b8d502-8190-40ab-b18f-88edd297d8b6"]) ) } - func testInput() throws { + func testInput() async throws { + let result = try await api.execute( + request: """ + mutation addUser($user: UserInput!) { + addUser(user: $user) { + id, + name + } + } + """, + context: api.context, + variables: ["user": ["id": "123", "name": "bob"]] + ) XCTAssertEqual( - try api.execute( - request: """ + result, + GraphQLResult( + data: ["addUser": ["id": "123", "name": "bob"]] + ) + ) + } + + func testInputRequest() async throws { + let result = try await api.execute( + request: GraphQLRequest( + query: """ mutation addUser($user: UserInput!) { addUser(user: $user) { id, @@ -272,64 +286,43 @@ class HelloWorldTests: XCTestCase { } } """, - context: api.context, - on: group, variables: ["user": ["id": "123", "name": "bob"]] - ).wait(), - GraphQLResult( - data: ["addUser": ["id": "123", "name": "bob"]] - ) + ), + context: api.context ) - } - - func testInputRequest() throws { XCTAssertEqual( - try api.execute( - request: GraphQLRequest( - query: """ - mutation addUser($user: UserInput!) { - addUser(user: $user) { - id, - name - } - } - """, - variables: ["user": ["id": "123", "name": "bob"]] - ), - context: api.context, - on: group - ).wait(), + result, GraphQLResult( data: ["addUser": ["id": "123", "name": "bob"]] ) ) } - func testInputRecursive() throws { - XCTAssertEqual( - try api.execute( - request: """ - mutation addUser($user: UserInput!) { - addUser(user: $user) { + func testInputRecursive() async throws { + let result = try await api.execute( + request: """ + mutation addUser($user: UserInput!) { + addUser(user: $user) { + id, + name, + friends { id, - name, - friends { - id, - name - } + name } } - """, - context: api.context, - on: group, - variables: [ - "user": [ - "id": "123", - "name": "bob", - "friends": [["id": "124", "name": "jeff"]], - ], - ] - ).wait(), + } + """, + context: api.context, + variables: [ + "user": [ + "id": "123", + "name": "bob", + "friends": [["id": "124", "name": "jeff"]], + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "addUser": [ diff --git a/Tests/GraphitiTests/PartialSchemaTests.swift b/Tests/GraphitiTests/PartialSchemaTests.swift index 0a38c66e..4bced791 100644 --- a/Tests/GraphitiTests/PartialSchemaTests.swift +++ b/Tests/GraphitiTests/PartialSchemaTests.swift @@ -1,6 +1,5 @@ import Graphiti import GraphQL -import NIO import XCTest class PartialSchemaTests: XCTestCase { @@ -12,7 +11,7 @@ class PartialSchemaTests: XCTestCase { .description("The id of the character.") Field("name", at: \.name) .description("The name of the character.") - Field("friends", at: \.friends) + Field("friends", at: Character.getFriends) .description( "The friends of the character, or an empty list if they have none." ) @@ -96,13 +95,7 @@ class PartialSchemaTests: XCTestCase { } } - func testPartialSchemaWithBuilder() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - try? group.syncShutdownGracefully() - } - + func testPartialSchemaWithBuilder() async throws { let builder = SchemaBuilder(StarWarsResolver.self, StarWarsContext.self) builder.use(partials: [BaseSchema(), SearchSchema()]) @@ -116,18 +109,18 @@ class PartialSchemaTests: XCTestCase { let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema) - XCTAssertEqual( - try api.execute( - request: """ - query { - human(id: "1000") { - name - } + let result = try await api.execute( + request: """ + query { + human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "human": [ "name": "Luke Skywalker", @@ -136,13 +129,7 @@ class PartialSchemaTests: XCTestCase { ) } - func testPartialSchema() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - try? group.syncShutdownGracefully() - } - + func testPartialSchema() async throws { /// Double check if static func works and the types are inferred properly let schema = try Schema.create(from: [BaseSchema(), SearchSchema()]) @@ -153,18 +140,18 @@ class PartialSchemaTests: XCTestCase { let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema) - XCTAssertEqual( - try api.execute( - request: """ - query { - human(id: "1000") { - name - } + let result = try await api.execute( + request: """ + query { + human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "human": [ "name": "Luke Skywalker", @@ -173,13 +160,7 @@ class PartialSchemaTests: XCTestCase { ) } - func testPartialSchemaOutOfOrder() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - try? group.syncShutdownGracefully() - } - + func testPartialSchemaOutOfOrder() async throws { /// Double check if ordering of partial schema doesn't matter let schema = try Schema.create(from: [SearchSchema(), BaseSchema()]) @@ -190,18 +171,18 @@ class PartialSchemaTests: XCTestCase { let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema) - XCTAssertEqual( - try api.execute( - request: """ - query { - human(id: "1000") { - name - } + let result = try await api.execute( + request: """ + query { + human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "human": [ "name": "Luke Skywalker", @@ -210,7 +191,7 @@ class PartialSchemaTests: XCTestCase { ) } - func testInstancePartialSchema() throws { + func testInstancePartialSchema() async throws { let baseSchema = PartialSchema( types: { Interface(Character.self) { @@ -218,7 +199,7 @@ class PartialSchemaTests: XCTestCase { .description("The id of the character.") Field("name", at: \.name) .description("The name of the character.") - Field("friends", at: \.friends) + Field("friends", at: Character.getFriends) .description( "The friends of the character, or an empty list if they have none." ) @@ -301,12 +282,6 @@ class PartialSchemaTests: XCTestCase { } ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - try? group.syncShutdownGracefully() - } - /// Double check if ordering of partial schema doesn't matter let schema = try Schema.create(from: [searchSchema, baseSchema]) @@ -317,18 +292,18 @@ class PartialSchemaTests: XCTestCase { let api = PartialSchemaTestAPI(resolver: StarWarsResolver(), schema: schema) - XCTAssertEqual( - try api.execute( - request: """ - query { - human(id: "1000") { - name - } + let result = try await api.execute( + request: """ + query { + human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "human": [ "name": "Luke Skywalker", diff --git a/Tests/GraphitiTests/ProductAPI/ProductResolver.swift b/Tests/GraphitiTests/ProductAPI/ProductResolver.swift index 635bdc75..4e748103 100644 --- a/Tests/GraphitiTests/ProductAPI/ProductResolver.swift +++ b/Tests/GraphitiTests/ProductAPI/ProductResolver.swift @@ -1,7 +1,6 @@ import Foundation import Graphiti import GraphQL -import NIO struct ProductResolver { var sdl: String diff --git a/Tests/GraphitiTests/ScalarTests.swift b/Tests/GraphitiTests/ScalarTests.swift index 3f0bb235..bcb386ec 100644 --- a/Tests/GraphitiTests/ScalarTests.swift +++ b/Tests/GraphitiTests/ScalarTests.swift @@ -1,13 +1,12 @@ import Foundation @testable import Graphiti import GraphQL -import NIO import XCTest class ScalarTests: XCTestCase { // MARK: Test UUID converts to String as expected - func testUUIDOutput() throws { + func testUUIDOutput() async throws { struct UUIDOutput { let value: UUID } @@ -32,21 +31,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - uuid { - value - } + let result = try await api.execute( + request: """ + query { + uuid { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "uuid": [ "value": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", @@ -55,7 +51,7 @@ class ScalarTests: XCTestCase { ) } - func testUUIDArg() throws { + func testUUIDArg() async throws { struct UUIDOutput { let value: UUID } @@ -86,21 +82,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - uuid (value: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F") { - value - } + let result = try await api.execute( + request: """ + query { + uuid (value: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F") { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "uuid": [ "value": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", @@ -109,7 +102,7 @@ class ScalarTests: XCTestCase { ) } - func testUUIDInput() throws { + func testUUIDInput() async throws { struct UUIDOutput { let value: UUID } @@ -147,21 +140,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - uuid (input: {value: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"}) { - value - } + let result = try await api.execute( + request: """ + query { + uuid (input: {value: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"}) { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "uuid": [ "value": "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", @@ -172,7 +162,7 @@ class ScalarTests: XCTestCase { // MARK: Test Date scalars convert to String using ISO8601 encoders - func testDateOutput() throws { + func testDateOutput() async throws { struct DateOutput { let value: Date } @@ -202,21 +192,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - date { - value - } + let result = try await api.execute( + request: """ + query { + date { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "date": [ "value": "2001-01-01T00:00:00Z", @@ -225,7 +212,7 @@ class ScalarTests: XCTestCase { ) } - func testDateArg() throws { + func testDateArg() async throws { struct DateOutput { let value: Date } @@ -261,21 +248,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - date (value: "2001-01-01T00:00:00Z") { - value - } + let result = try await api.execute( + request: """ + query { + date (value: "2001-01-01T00:00:00Z") { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "date": [ "value": "2001-01-01T00:00:00Z", @@ -284,7 +268,7 @@ class ScalarTests: XCTestCase { ) } - func testDateInput() throws { + func testDateInput() async throws { struct DateOutput { let value: Date } @@ -327,21 +311,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - date (input: {value: "2001-01-01T00:00:00Z"}) { - value - } + let result = try await api.execute( + request: """ + query { + date (input: {value: "2001-01-01T00:00:00Z"}) { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "date": [ "value": "2001-01-01T00:00:00Z", @@ -352,7 +333,7 @@ class ScalarTests: XCTestCase { // MARK: Test a scalar that converts to a single-value Map (StringCodedCoordinate -> String) - func testStringCoordOutput() throws { + func testStringCoordOutput() async throws { struct CoordinateOutput { let value: StringCodedCoordinate } @@ -377,21 +358,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - coord { - value - } + let result = try await api.execute( + request: """ + query { + coord { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "coord": [ "value": "(0.0, 0.0)", @@ -400,7 +378,7 @@ class ScalarTests: XCTestCase { ) } - func testStringCoordArg() throws { + func testStringCoordArg() async throws { struct CoordinateOutput { let value: StringCodedCoordinate } @@ -431,21 +409,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - coord (value: "(0.0, 0.0)") { - value - } + let result = try await api.execute( + request: """ + query { + coord (value: "(0.0, 0.0)") { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "coord": [ "value": "(0.0, 0.0)", @@ -454,7 +429,7 @@ class ScalarTests: XCTestCase { ) } - func testStringCoordInput() throws { + func testStringCoordInput() async throws { struct CoordinateOutput { let value: StringCodedCoordinate } @@ -492,21 +467,18 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - coord (input: {value: "(0.0, 0.0)"}) { - value - } + let result = try await api.execute( + request: """ + query { + coord (input: {value: "(0.0, 0.0)"}) { + value } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "coord": [ "value": "(0.0, 0.0)", @@ -517,7 +489,7 @@ class ScalarTests: XCTestCase { // MARK: Test a scalar that converts to a multi-value Map (Coordinate -> Dict) - func testDictCoordOutput() throws { + func testDictCoordOutput() async throws { struct CoordinateOutput { let value: DictCodedCoordinate } @@ -542,11 +514,8 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - // Test individual fields because we can't be confident we'll match the ordering of Map's OrderedDictionary - let result = try api.execute( + let result = try await api.execute( request: """ query { coord { @@ -554,9 +523,9 @@ class ScalarTests: XCTestCase { } } """, - context: NoContext(), - on: group - ).wait() + context: NoContext() + ) + let value = result.data?.dictionary?["coord"]?.dictionary?["value"]?.dictionary XCTAssertEqual( @@ -569,7 +538,7 @@ class ScalarTests: XCTestCase { ) } - func testDictCoordArg() throws { + func testDictCoordArg() async throws { struct CoordinateOutput { let value: DictCodedCoordinate } @@ -600,11 +569,8 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - // Test individual fields because we can't be confident we'll match the ordering of Map's OrderedDictionary - let result = try api.execute( + let result = try await api.execute( request: """ query { coord (value: {latitude: 0.0, longitude: 0.0}) { @@ -612,9 +578,9 @@ class ScalarTests: XCTestCase { } } """, - context: NoContext(), - on: group - ).wait() + context: NoContext() + ) + let value = result.data?.dictionary?["coord"]?.dictionary?["value"]?.dictionary XCTAssertEqual( @@ -627,7 +593,7 @@ class ScalarTests: XCTestCase { ) } - func testDictCoordInput() throws { + func testDictCoordInput() async throws { struct CoordinateOutput { let value: DictCodedCoordinate } @@ -665,11 +631,8 @@ class ScalarTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - // Test individual fields because we can't be confident we'll match the ordering of Map's OrderedDictionary - let result = try api.execute( + let result = try await api.execute( request: """ query { coord (input: {value: {latitude: 0.0, longitude: 0.0}}) { @@ -677,9 +640,9 @@ class ScalarTests: XCTestCase { } } """, - context: NoContext(), - on: group - ).wait() + context: NoContext() + ) + let value = result.data?.dictionary?["coord"]?.dictionary?["value"]?.dictionary XCTAssertEqual( @@ -693,7 +656,7 @@ class ScalarTests: XCTestCase { } } -private class TestAPI: API { +private class TestAPI: API { public let resolver: Resolver public let schema: Schema diff --git a/Tests/GraphitiTests/SchemaBuilderTests.swift b/Tests/GraphitiTests/SchemaBuilderTests.swift index e531e57d..8e8db85c 100644 --- a/Tests/GraphitiTests/SchemaBuilderTests.swift +++ b/Tests/GraphitiTests/SchemaBuilderTests.swift @@ -1,12 +1,9 @@ import Graphiti import GraphQL -import NIO import XCTest class SchemaBuilderTests: XCTestCase { - func testSchemaBuilder() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - + func testSchemaBuilder() async throws { let builder = SchemaBuilder(StarWarsResolver.self, StarWarsContext.self) // Add assets slightly out of order @@ -70,7 +67,7 @@ class SchemaBuilderTests: XCTestCase { .description("The id of the character.") Field("name", at: \.name) .description("The name of the character.") - Field("friends", at: \.friends) + Field("friends", at: Character.getFriends) .description( "The friends of the character, or an empty list if they have none." ) @@ -104,18 +101,18 @@ class SchemaBuilderTests: XCTestCase { let api = SchemaBuilderTestAPI(resolver: StarWarsResolver(), schema: schema) - XCTAssertEqual( - try api.execute( - request: """ - query { - human(id: "1000") { - name - } + let result = try await api.execute( + request: """ + query { + human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "human": [ "name": "Luke Skywalker", diff --git a/Tests/GraphitiTests/SchemaTests.swift b/Tests/GraphitiTests/SchemaTests.swift index b4db8ce8..2ac84932 100644 --- a/Tests/GraphitiTests/SchemaTests.swift +++ b/Tests/GraphitiTests/SchemaTests.swift @@ -1,12 +1,11 @@ import Foundation @testable import Graphiti import GraphQL -import NIO import XCTest class SchemaTests: XCTestCase { // Tests that circularly dependent objects can be used in schema and resolved correctly - func testCircularDependencies() throws { + func testCircularDependencies() async throws { struct A: Codable { let name: String var b: B { @@ -45,23 +44,20 @@ class SchemaTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - a { - b { - name - } - } + let result = try await api.execute( + request: """ + query { + a { + b { + name + } } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "a": [ "b": [ @@ -73,7 +69,7 @@ class SchemaTests: XCTestCase { } // Tests that we can resolve type references for named types - func testTypeReferenceForNamedType() throws { + func testTypeReferenceForNamedType() async throws { struct LocationObject: Codable { let id: String let name: String @@ -114,23 +110,20 @@ class SchemaTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - user { - location { - name - } - } + let result = try await api.execute( + request: """ + query { + user { + location { + name + } } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "user": [ "location": [ @@ -149,7 +142,7 @@ class SchemaTests: XCTestCase { struct TestResolver {} do { - let _ = try Schema { + _ = try Schema { Type(User.self) { Field("id", at: \.id) } @@ -165,7 +158,7 @@ class SchemaTests: XCTestCase { } } -private class TestAPI: API { +private class TestAPI: API { public let resolver: Resolver public let schema: Schema diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift index 33a8e4fe..cf3af11c 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsContext.swift @@ -5,8 +5,8 @@ * fetching this data from a backend service rather than from hardcoded * values in a more complex demo. */ -public final class StarWarsContext { - private static var tatooine = Planet( +public final class StarWarsContext: Sendable { + private static let tatooine = Planet( id: "10001", name: "Tatooine", diameter: 10465, @@ -15,7 +15,7 @@ public final class StarWarsContext { residents: [] ) - private static var alderaan = Planet( + private static let alderaan = Planet( id: "10002", name: "Alderaan", diameter: 12500, @@ -24,12 +24,12 @@ public final class StarWarsContext { residents: [] ) - private static var planetData: [String: Planet] = [ + private static let planetData: [String: Planet] = [ "10001": tatooine, "10002": alderaan, ] - private static var luke = Human( + private static let luke = Human( id: "1000", name: "Luke Skywalker", friends: ["1002", "1003", "2000", "2001"], @@ -37,7 +37,7 @@ public final class StarWarsContext { homePlanet: tatooine ) - private static var vader = Human( + private static let vader = Human( id: "1001", name: "Darth Vader", friends: ["1004"], @@ -45,7 +45,7 @@ public final class StarWarsContext { homePlanet: tatooine ) - private static var han = Human( + private static let han = Human( id: "1002", name: "Han Solo", friends: ["1000", "1003", "2001"], @@ -53,7 +53,7 @@ public final class StarWarsContext { homePlanet: alderaan ) - private static var leia = Human( + private static let leia = Human( id: "1003", name: "Leia Organa", friends: ["1000", "1002", "2000", "2001"], @@ -61,7 +61,7 @@ public final class StarWarsContext { homePlanet: alderaan ) - private static var tarkin = Human( + private static let tarkin = Human( id: "1004", name: "Wilhuff Tarkin", friends: ["1001"], @@ -69,7 +69,7 @@ public final class StarWarsContext { homePlanet: alderaan ) - private static var humanData: [String: Human] = [ + private static let humanData: [String: Human] = [ "1000": luke, "1001": vader, "1002": han, @@ -77,7 +77,7 @@ public final class StarWarsContext { "1004": tarkin, ] - private static var c3po = Droid( + private static let c3po = Droid( id: "2000", name: "C-3PO", friends: ["1000", "1002", "1003", "2001"], @@ -85,7 +85,7 @@ public final class StarWarsContext { primaryFunction: "Protocol" ) - private static var r2d2 = Droid( + private static let r2d2 = Droid( id: "2001", name: "R2-D2", friends: ["1000", "1002", "1003"], @@ -93,7 +93,7 @@ public final class StarWarsContext { primaryFunction: "Astromech" ) - private static var droidData: [String: Droid] = [ + private static let droidData: [String: Droid] = [ "2000": c3po, "2001": r2d2, ] diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift index 556caf17..f21beeb6 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsEntities.swift @@ -1,17 +1,17 @@ -public enum Episode: String, CaseIterable, Codable { +public enum Episode: String, CaseIterable, Codable, Sendable { case newHope = "NEWHOPE" case empire = "EMPIRE" case jedi = "JEDI" } -public protocol Character { +public protocol Character: Sendable { var id: String { get } var name: String { get } var friends: [String] { get } var appearsIn: [Episode] { get } } -public protocol SearchResult {} +public protocol SearchResult: Sendable {} public struct Planet: SearchResult { public let id: String diff --git a/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift b/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift index 9af54e14..b9c5c82e 100644 --- a/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift +++ b/Tests/GraphitiTests/StarWarsAPI/StarWarsResolver.swift @@ -30,10 +30,10 @@ public extension Droid { } } -public struct StarWarsResolver { +public struct StarWarsResolver: Sendable { public init() {} - public struct HeroArguments: Codable { + public struct HeroArguments: Codable, Sendable { public let episode: Episode? } @@ -41,7 +41,7 @@ public struct StarWarsResolver { context.getHero(of: arguments.episode) } - public struct HumanArguments: Codable { + public struct HumanArguments: Codable, Sendable { public let id: String } @@ -49,7 +49,7 @@ public struct StarWarsResolver { context.getHuman(id: arguments.id) } - public struct DroidArguments: Codable { + public struct DroidArguments: Codable, Sendable { public let id: String } @@ -57,7 +57,7 @@ public struct StarWarsResolver { context.getDroid(id: arguments.id) } - public struct SearchArguments: Codable { + public struct SearchArguments: Codable, Sendable { public let query: String } diff --git a/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift b/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift index 4cdf31fa..6dd85dce 100644 --- a/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift +++ b/Tests/GraphitiTests/StarWarsTests/StarWarsIntrospectionTests.swift @@ -1,32 +1,26 @@ import GraphQL -import NIO import XCTest @testable import Graphiti class StarWarsIntrospectionTests: XCTestCase { private let api = StarWarsAPI() - private let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - deinit { - try? group.syncShutdownGracefully() - } - - func testIntrospectionTypeQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionTypeQuery { - __schema { - types { - name - } + func testIntrospectionTypeQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionTypeQuery { + __schema { + types { + name } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__schema": [ @@ -92,21 +86,21 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionQueryTypeQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionQueryTypeQuery { - __schema { - queryType { - name - } + func testIntrospectionQueryTypeQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionQueryTypeQuery { + __schema { + queryType { + name } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__schema": [ @@ -119,19 +113,19 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionDroidTypeQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionDroidTypeQuery { - __type(name: \"Droid\") { - name - } + func testIntrospectionDroidTypeQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionDroidTypeQuery { + __type(name: \"Droid\") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__type": [ @@ -142,20 +136,20 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionDroidKindQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionDroidKindQuery { - __type(name: \"Droid\") { - name - kind - } + func testIntrospectionDroidKindQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionDroidKindQuery { + __type(name: \"Droid\") { + name + kind } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__type": [ @@ -167,20 +161,20 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionCharacterKindQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionCharacterKindQuery { - __type(name: \"Character\") { - name - kind - } + func testIntrospectionCharacterKindQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionCharacterKindQuery { + __type(name: \"Character\") { + name + kind } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__type": [ @@ -192,26 +186,26 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionDroidFieldsQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionDroidFieldsQuery { - __type(name: \"Droid\") { + func testIntrospectionDroidFieldsQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionDroidFieldsQuery { + __type(name: \"Droid\") { + name + fields { name - fields { + type { name - type { - name - kind - } + kind } } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__type": [ @@ -266,30 +260,30 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionDroidNestedFieldsQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionDroidNestedFieldsQuery { - __type(name: \"Droid\") { + func testIntrospectionDroidNestedFieldsQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionDroidNestedFieldsQuery { + __type(name: \"Droid\") { + name + fields { name - fields { + type { name - type { + kind + ofType { name kind - ofType { - name - kind - } } } } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__type": [ @@ -365,36 +359,36 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionFieldArgsQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionFieldArgsQuery { - __schema { - queryType { - fields { + func testIntrospectionFieldArgsQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionFieldArgsQuery { + __schema { + queryType { + fields { + name + args { name - args { + description + type { name - description - type { + kind + ofType { name kind - ofType { - name - kind - } } - defaultValue - } - } + } + defaultValue + } } } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__schema": [ @@ -477,20 +471,20 @@ class StarWarsIntrospectionTests: XCTestCase { ) } - func testIntrospectionDroidDescriptionQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query IntrospectionDroidDescriptionQuery { - __type(name: \"Droid\") { - name - description - } + func testIntrospectionDroidDescriptionQuery() async throws { + let result = try await api.execute( + request: """ + query IntrospectionDroidDescriptionQuery { + __type(name: \"Droid\") { + name + description } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "__type": [ diff --git a/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift b/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift index 33b68750..bb667a57 100644 --- a/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift +++ b/Tests/GraphitiTests/StarWarsTests/StarWarsQueryTests.swift @@ -1,50 +1,44 @@ @testable import Graphiti import GraphQL -import NIO import XCTest class StarWarsQueryTests: XCTestCase { private let api = StarWarsAPI() - private var group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - deinit { - try? self.group.syncShutdownGracefully() - } - - func testHeroNameQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query HeroNameQuery { - hero { - name - } + func testHeroNameQuery() async throws { + let result = try await api.execute( + request: """ + query HeroNameQuery { + hero { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult(data: ["hero": ["name": "R2-D2"]]) ) } - func testHeroNameAndFriendsQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query HeroNameAndFriendsQuery { - hero { - id + func testHeroNameAndFriendsQuery() async throws { + let result = try await api.execute( + request: """ + query HeroNameAndFriendsQuery { + hero { + id + name + friends { name - friends { - name - } } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -61,26 +55,26 @@ class StarWarsQueryTests: XCTestCase { ) } - func testNestedQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query NestedQuery { - hero { + func testNestedQuery() async throws { + let result = try await api.execute( + request: """ + query NestedQuery { + hero { + name + friends { name + appearsIn friends { name - appearsIn - friends { - name - } } } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -122,19 +116,19 @@ class StarWarsQueryTests: XCTestCase { ) } - func testFetchLukeQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query FetchLukeQuery { - human(id: "1000") { - name - } + func testFetchLukeQuery() async throws { + let result = try await api.execute( + request: """ + query FetchLukeQuery { + human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "human": [ @@ -145,20 +139,20 @@ class StarWarsQueryTests: XCTestCase { ) } - func testFetchSomeIDQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } + func testFetchSomeIDQuery() async throws { + var result = try await api.execute( + request: """ + query FetchSomeIDQuery($someId: String!) { + human(id: $someId) { + name } - """, - context: StarWarsContext(), - on: group, - variables: ["someId": "1000"] - ).wait(), + } + """, + context: StarWarsContext(), + variables: ["someId": "1000"] + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "human": [ @@ -168,19 +162,19 @@ class StarWarsQueryTests: XCTestCase { ) ) - XCTAssertEqual( - try api.execute( - request: """ - query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } + result = try await api.execute( + request: """ + query FetchSomeIDQuery($someId: String!) { + human(id: $someId) { + name } - """, - context: StarWarsContext(), - on: group, - variables: ["someId": "1002"] - ).wait(), + } + """, + context: StarWarsContext(), + variables: ["someId": "1002"] + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "human": [ @@ -190,19 +184,19 @@ class StarWarsQueryTests: XCTestCase { ) ) - XCTAssertEqual( - try api.execute( - request: """ - query FetchSomeIDQuery($someId: String!) { - human(id: $someId) { - name - } + result = try await api.execute( + request: """ + query FetchSomeIDQuery($someId: String!) { + human(id: $someId) { + name } - """, - context: StarWarsContext(), - on: group, - variables: ["someId": "not a valid id"] - ).wait(), + } + """, + context: StarWarsContext(), + variables: ["someId": "not a valid id"] + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "human": nil, @@ -211,19 +205,19 @@ class StarWarsQueryTests: XCTestCase { ) } - func testFetchLukeAliasedQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query FetchLukeAliasedQuery { - luke: human(id: "1000") { - name - } + func testFetchLukeAliasedQuery() async throws { + let result = try await api.execute( + request: """ + query FetchLukeAliasedQuery { + luke: human(id: "1000") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "luke": [ @@ -234,22 +228,22 @@ class StarWarsQueryTests: XCTestCase { ) } - func testFetchLukeAndLeiaAliasedQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query FetchLukeAndLeiaAliasedQuery { - luke: human(id: "1000") { - name - } - leia: human(id: "1003") { - name - } + func testFetchLukeAndLeiaAliasedQuery() async throws { + let result = try await api.execute( + request: """ + query FetchLukeAndLeiaAliasedQuery { + luke: human(id: "1000") { + name + } + leia: human(id: "1003") { + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "luke": [ @@ -263,24 +257,24 @@ class StarWarsQueryTests: XCTestCase { ) } - func testDuplicateFieldsQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query DuplicateFieldsQuery { - luke: human(id: "1000") { - name - homePlanet { name } - } - leia: human(id: "1003") { - name - homePlanet { name } - } + func testDuplicateFieldsQuery() async throws { + let result = try await api.execute( + request: """ + query DuplicateFieldsQuery { + luke: human(id: "1000") { + name + homePlanet { name } + } + leia: human(id: "1003") { + name + homePlanet { name } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "luke": [ @@ -296,26 +290,26 @@ class StarWarsQueryTests: XCTestCase { ) } - func testUseFragmentQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query UseFragmentQuery { - luke: human(id: "1000") { - ...HumanFragment - } - leia: human(id: "1003") { - ...HumanFragment - } + func testUseFragmentQuery() async throws { + let result = try await api.execute( + request: """ + query UseFragmentQuery { + luke: human(id: "1000") { + ...HumanFragment } - fragment HumanFragment on Human { - name - homePlanet { name } + leia: human(id: "1003") { + ...HumanFragment } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + fragment HumanFragment on Human { + name + homePlanet { name } + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "luke": [ @@ -331,20 +325,20 @@ class StarWarsQueryTests: XCTestCase { ) } - func testCheckTypeOfR2Query() throws { - XCTAssertEqual( - try api.execute( - request: """ - query CheckTypeOfR2Query { - hero { - __typename - name - } + func testCheckTypeOfR2Query() async throws { + let result = try await api.execute( + request: """ + query CheckTypeOfR2Query { + hero { + __typename + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -356,20 +350,20 @@ class StarWarsQueryTests: XCTestCase { ) } - func testCheckTypeOfLukeQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query CheckTypeOfLukeQuery { - hero(episode: EMPIRE) { - __typename - name - } + func testCheckTypeOfLukeQuery() async throws { + let result = try await api.execute( + request: """ + query CheckTypeOfLukeQuery { + hero(episode: EMPIRE) { + __typename + name } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -381,20 +375,20 @@ class StarWarsQueryTests: XCTestCase { ) } - func testSecretBackstoryQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query SecretBackstoryQuery { - hero { - name - secretBackstory - } + func testSecretBackstoryQuery() async throws { + let result = try await api.execute( + request: """ + query SecretBackstoryQuery { + hero { + name + secretBackstory } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -413,23 +407,23 @@ class StarWarsQueryTests: XCTestCase { ) } - func testSecretBackstoryListQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query SecretBackstoryListQuery { - hero { + func testSecretBackstoryListQuery() async throws { + let result = try await api.execute( + request: """ + query SecretBackstoryListQuery { + hero { + name + friends { name - friends { - name - secretBackstory - } + secretBackstory } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -471,20 +465,20 @@ class StarWarsQueryTests: XCTestCase { ) } - func testSecretBackstoryAliasQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query SecretBackstoryAliasQuery { - mainHero: hero { - name - story: secretBackstory - } + func testSecretBackstoryAliasQuery() async throws { + let result = try await api.execute( + request: """ + query SecretBackstoryAliasQuery { + mainHero: hero { + name + story: secretBackstory } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "mainHero": [ @@ -503,7 +497,7 @@ class StarWarsQueryTests: XCTestCase { ) } - func testNonNullableFieldsQuery() throws { + func testNonNullableFieldsQuery() async throws { struct A { func nullableA(context _: NoContext, arguments _: NoArguments) -> A? { return A() @@ -545,24 +539,24 @@ class StarWarsQueryTests: XCTestCase { } let api = MyAPI() - XCTAssertEqual( - try api.execute( - request: """ - query { + let result = try await api.execute( + request: """ + query { + nullableA { nullableA { - nullableA { + nonNullA { nonNullA { - nonNullA { - throws - } + throws } } } } - """, - context: NoContext(), - on: group - ).wait(), + } + """, + context: NoContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "nullableA": [ @@ -580,29 +574,29 @@ class StarWarsQueryTests: XCTestCase { ) } - func testSearchQuery() throws { - XCTAssertEqual( - try api.execute( - request: """ - query { - search(query: "o") { - ... on Planet { - name - diameter - } - ... on Human { - name - } - ... on Droid { - name - primaryFunction - } + func testSearchQuery() async throws { + let result = try await api.execute( + request: """ + query { + search(query: "o") { + ... on Planet { + name + diameter + } + ... on Human { + name + } + ... on Droid { + name + primaryFunction } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "search": [ @@ -616,23 +610,23 @@ class StarWarsQueryTests: XCTestCase { ) } - func testDirective() throws { - XCTAssertEqual( - try api.execute( - request: """ - query Hero { - hero { - name + func testDirective() async throws { + var result = try await api.execute( + request: """ + query Hero { + hero { + name - friends @include(if: false) { - name - } + friends @include(if: false) { + name } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ @@ -642,22 +636,22 @@ class StarWarsQueryTests: XCTestCase { ) ) - XCTAssertEqual( - try api.execute( - request: """ - query Hero { - hero { - name + result = try await api.execute( + request: """ + query Hero { + hero { + name - friends @include(if: true) { - name - } + friends @include(if: true) { + name } } - """, - context: StarWarsContext(), - on: group - ).wait(), + } + """, + context: StarWarsContext() + ) + XCTAssertEqual( + result, GraphQLResult( data: [ "hero": [ diff --git a/Tests/GraphitiTests/UnionTests.swift b/Tests/GraphitiTests/UnionTests.swift index 33209c20..4a832515 100644 --- a/Tests/GraphitiTests/UnionTests.swift +++ b/Tests/GraphitiTests/UnionTests.swift @@ -1,7 +1,6 @@ import Foundation @testable import Graphiti import GraphQL -import NIO import XCTest class UnionTests: XCTestCase { diff --git a/Tests/GraphitiTests/ValidationRulesTests.swift b/Tests/GraphitiTests/ValidationRulesTests.swift index 8648ad0c..ecf166bf 100644 --- a/Tests/GraphitiTests/ValidationRulesTests.swift +++ b/Tests/GraphitiTests/ValidationRulesTests.swift @@ -1,12 +1,11 @@ import Foundation @testable import Graphiti import GraphQL -import NIO import XCTest class ValidationRulesTests: XCTestCase { // Test registering custom validation rules - func testRegisteringCustomValidationRule() throws { + func testRegisteringCustomValidationRule() async throws { struct TestResolver { var helloWorld: String { "Hellow World" } } @@ -21,23 +20,20 @@ class ValidationRulesTests: XCTestCase { schema: testSchema ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { try? group.syncShutdownGracefully() } - - XCTAssertEqual( - try api.execute( - request: """ - query { - __type(name: "Query") { - name - description - } + let result = try await api.execute( + request: """ + query { + __type(name: "Query") { + name + description } - """, - context: NoContext(), - on: group, - validationRules: [NoIntrospectionRule] - ).wait(), + } + """, + context: NoContext(), + validationRules: [NoIntrospectionRule] + ) + XCTAssertEqual( + result, GraphQLResult(errors: [ .init( message: "GraphQL introspection is not allowed, but the query contained __schema or __type", @@ -48,7 +44,7 @@ class ValidationRulesTests: XCTestCase { } } -private class TestAPI: API { +private class TestAPI: API { public let resolver: Resolver public let schema: Schema diff --git a/UsageGuide.md b/UsageGuide.md index be295f25..64981cf5 100644 --- a/UsageGuide.md +++ b/UsageGuide.md @@ -9,9 +9,6 @@ Here is an example of a basic `"Hello world"` GraphQL schema: ```swift import Graphiti -import NIO - -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) struct HelloResolver { func hello(context: NoContext, arguments: NoArguments) -> String { @@ -35,8 +32,7 @@ This schema can be queried in Swift using the `execute` function. : ```swift let result = try await HelloAPI().execute( request: "{ hello }", - context: NoContext(), - on: eventLoopGroup + context: NoContext() ) print(result) ``` @@ -53,9 +49,6 @@ Graphiti includes support for using Swift types in the schema itself. To connect ```swift import Graphiti -import NIO - -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) struct Person: Codable { let name: String @@ -98,8 +91,7 @@ let result = try await PointBreakAPI().execute( } } """, - context: NoContext(), - on: eventLoopGroup + context: NoContext() ) ``` @@ -336,7 +328,7 @@ import GraphQL let timer: Timer! -struct PersonResolver { +struct PersonResolver: Sendable { func people(context: NoContext, arguments: NoArguments) -> [Person] { return characters } @@ -382,11 +374,10 @@ This schema can be subscribed to in Swift using the `subscribe` function. The ex let api = PointBreakAPI() let stream = try await api.subscribe( request: "subscription { fiftyYearStormAlert }", - context: NoContext(), - on: eventLoopGroup -).stream! -let resultStream = stream.map { result in - try print(result.wait()) + context: NoContext() +) +for try await event in stream { + try print(event) } ``` @@ -409,7 +400,7 @@ This package supports pagination using the [Relay-based GraphQL Cursor Connectio Here's an example using the schema above: ```swift -struct Person: Codable, Identifiable { +struct Person: Codable, Identifiable, Sendable { let id: Int let name: String } @@ -419,7 +410,7 @@ let characters = [ Person(id: 2, name: "Bodhi"), ] -struct PersonResolver { +struct PersonResolver: Sendable { func people(context: NoContext, arguments: PaginationArguments) throws -> Connection { return try characters.connection(from: arguments) } @@ -504,7 +495,7 @@ The result of this query is a `GraphQLResult` that encodes to the following JSON Federation allows you split your GraphQL API into smaller services and link them back together so clients see a single larger API. More information can be found [here](https://www.apollographql.com/docs/federation). To enable federation you must: 1. Define `Keys` on the entity types, which specify the primary key fields and the resolver function used to load an entity from that key. -2. Provide the schema SDL to the schema itself. +2. Provide the schema SDL to the schema itself. Here's an example for the following schema: @@ -532,30 +523,29 @@ extend type User @key(fields: "email") { ```swift import Foundation import Graphiti -import NIO -struct Product: Codable { +struct Product: Codable, Sendable { let id: String let sku: String let createdBy: User } -struct User: Codable { +struct User: Codable, Sendable { let email: String let name: String? let totalProductsCreated: Int? let yearsOfEmployment: Int } -struct ProductContext { +struct ProductContext: Sendable { func getUser(email: String) -> User { ... } } -struct ProductResolver { - struct UserArguments: Codable { +struct ProductResolver: Sendable { + struct UserArguments: Codable, Sendable { let email: String } - + func user(context: ProductContext, arguments: UserArguments) -> User? { context.getUser(email: arguments.email) } @@ -569,7 +559,7 @@ final class ProductSchema: PartialSchema { Field("sku", at: \.sku) Field("createdBy", at: \.createdBy) } - + Type( User.self, keys: { @@ -597,9 +587,8 @@ let schema = try SchemaBuilder(ProductResolver.self, ProductContext.self) .build() let api = ProductAPI(resolver: ProductResolver(), schema: schema) -let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) -api.execute( +try await api.execute( request: """ query { _entities(representations: {__typename: "User", email: "abc@def.com"}) { @@ -610,7 +599,6 @@ api.execute( } } """, - context: ProductContext(), - on: group + context: ProductContext() ) ```