From decc5b8ed761743de4f572af183c895980230927 Mon Sep 17 00:00:00 2001 From: Petro Rovenskyi Date: Wed, 16 Oct 2024 11:37:33 +0300 Subject: [PATCH 1/2] - bumped swift-tools to 5.8. - Adjusted CI to drop swift version less than 5.8 - run `swiftformat` locally --- .github/workflows/build.yml | 16 +- Package.resolved | 42 +- Package.swift | 2 +- Sources/GraphQL/GraphQL.swift | 262 ++- .../GraphQL/Subscription/EventStream.swift | 103 +- .../HelloWorldTests/HelloWorldTests.swift | 34 +- .../SubscriptionTests/SimplePubSub.swift | 68 +- .../SubscriptionSchema.swift | 396 ++-- .../SubscriptionTests/SubscriptionTests.swift | 1783 ++++++++--------- 9 files changed, 1332 insertions(+), 1374 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 956e39ed..e30891b3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -61,7 +61,7 @@ jobs: runs-on: ubuntu-22.04 strategy: matrix: - swift: ["5.7", "5.8", "5.9", "5.10"] + swift: ["5.8", "5.9", "5.10"] steps: - uses: swift-actions/setup-swift@v2 with: @@ -70,17 +70,3 @@ jobs: - name: Test run: swift test --parallel - # Swift versions older than 5.7 don't have builds for 22.04. https://www.swift.org/download/ - backcompat-ubuntu-20_04: - name: Test Swift ${{ matrix.swift }} on Ubuntu 20.04 - runs-on: ubuntu-20.04 - strategy: - matrix: - swift: ["5.4", "5.5", "5.6"] - steps: - - uses: swift-actions/setup-swift@v2 - with: - swift-version: ${{ matrix.swift }} - - uses: actions/checkout@v3 - - name: Test - run: swift test --parallel diff --git a/Package.resolved b/Package.resolved index 7efdac90..838f977e 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,25 +1,23 @@ { - "object": { - "pins": [ - { - "package": "swift-collections", - "repositoryURL": "https://github.com/apple/swift-collections", - "state": { - "branch": null, - "revision": "48254824bb4248676bf7ce56014ff57b142b77eb", - "version": "1.0.2" - } - }, - { - "package": "swift-nio", - "repositoryURL": "https://github.com/apple/swift-nio.git", - "state": { - "branch": null, - "revision": "124119f0bb12384cef35aa041d7c3a686108722d", - "version": "2.40.0" - } + "pins" : [ + { + "identity" : "swift-collections", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-collections", + "state" : { + "revision" : "9bf03ff58ce34478e66aaee630e491823326fd06", + "version" : "1.1.3" } - ] - }, - "version": 1 + }, + { + "identity" : "swift-nio", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-nio.git", + "state" : { + "revision" : "4c4453b489cf76e6b3b0f300aba663eb78182fad", + "version" : "2.70.0" + } + } + ], + "version" : 2 } diff --git a/Package.swift b/Package.swift index 019a2e86..2838065f 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.4 +// swift-tools-version:5.8 import PackageDescription let package = Package( diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index ebe959e3..0c192fe7 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -279,137 +279,133 @@ public func graphqlSubscribe( // MARK: Async/Await -#if compiler(>=5.5) && canImport(_Concurrency) - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - /// This is the primary entry point function for fulfilling GraphQL operations - /// by parsing, validating, and executing a GraphQL document along side a - /// GraphQL schema. - /// - /// More sophisticated GraphQL servers, such as those which persist queries, - /// may wish to separate the validation and execution phases to a static time - /// tooling step, and a server runtime step. - /// - /// - parameter queryStrategy: The field execution strategy to use for query requests - /// - parameter mutationStrategy: The field execution strategy to use for mutation requests - /// - parameter subscriptionStrategy: The field execution strategy to use for subscription - /// requests - /// - parameter instrumentation: The instrumentation implementation to call during the - /// parsing, validating, execution, and field resolution stages. - /// - parameter schema: The GraphQL type system to use when validating and - /// executing a query. - /// - parameter request: A GraphQL language formatted string representing the - /// requested operation. - /// - parameter rootValue: The value provided as the first argument to resolver - /// functions on the top level type (e.g. the query object type). - /// - parameter contextValue: A context value provided to all resolver functions - /// functions - /// - parameter variableValues: A mapping of variable name to runtime value to use for all - /// variables defined in the `request`. - /// - parameter operationName: The name of the operation to use if `request` contains - /// multiple possible operations. Can be omitted if `request` contains only one operation. - /// - /// - throws: throws GraphQLError if an error occurs while parsing the `request`. - /// - /// - returns: returns a `Map` dictionary containing the result of the query inside the key - /// `data` and any validation or execution errors inside the key `errors`. The value of `data` - /// might be `null` if, for example, the query is invalid. It's possible to have both `data` and - /// `errors` if an error occurs only in a specific field. If that happens the value of that - /// field will be `null` and there will be an error inside `errors` specifying the reason for - /// the failure and the path of the failed field. - public func graphql( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), - mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, - schema: GraphQLSchema, - request: String, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, - variableValues: [String: Map] = [:], - operationName: String? = nil - ) async throws -> GraphQLResult { - return try await graphql( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - request: request, - rootValue: rootValue, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ).get() - } - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - /// This is the primary entry point function for fulfilling GraphQL subscription - /// operations by parsing, validating, and executing a GraphQL subscription - /// document along side a GraphQL schema. - /// - /// More sophisticated GraphQL servers, such as those which persist queries, - /// may wish to separate the validation and execution phases to a static time - /// tooling step, and a server runtime step. - /// - /// - parameter queryStrategy: The field execution strategy to use for query requests - /// - parameter mutationStrategy: The field execution strategy to use for mutation requests - /// - parameter subscriptionStrategy: The field execution strategy to use for subscription - /// requests - /// - parameter instrumentation: The instrumentation implementation to call during the - /// parsing, validating, execution, and field resolution stages. - /// - parameter schema: The GraphQL type system to use when validating and - /// executing a query. - /// - parameter request: A GraphQL language formatted string representing the - /// requested operation. - /// - parameter rootValue: The value provided as the first argument to resolver - /// functions on the top level type (e.g. the query object type). - /// - parameter contextValue: A context value provided to all resolver functions - /// - parameter variableValues: A mapping of variable name to runtime value to use for all - /// variables defined in the `request`. - /// - parameter operationName: The name of the operation to use if `request` contains - /// multiple possible operations. Can be omitted if `request` contains only one operation. - /// - /// - throws: throws GraphQLError if an error occurs while parsing the `request`. - /// - /// - returns: returns a SubscriptionResult containing the subscription observable inside the - /// key `observable` and any validation or execution errors inside the key `errors`. The - /// value of `observable` might be `null` if, for example, the query is invalid. It's not - /// possible to have both `observable` and `errors`. The observable payloads are - /// GraphQLResults which contain the result of the query inside the key `data` and any - /// validation or execution errors inside the key `errors`. The value of `data` might be `null`. - /// It's possible to have both `data` and `errors` if an error occurs only in a specific field. - /// If that happens the value of that field will be `null` and there - /// will be an error inside `errors` specifying the reason for the failure and the path of the - /// failed field. - public func graphqlSubscribe( - queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), - mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), - subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, - schema: GraphQLSchema, - request: String, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, - variableValues: [String: Map] = [:], - operationName: String? = nil - ) async throws -> SubscriptionResult { - return try await graphqlSubscribe( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - request: request, - rootValue: rootValue, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ).get() - } +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +/// This is the primary entry point function for fulfilling GraphQL operations +/// by parsing, validating, and executing a GraphQL document along side a +/// GraphQL schema. +/// +/// More sophisticated GraphQL servers, such as those which persist queries, +/// may wish to separate the validation and execution phases to a static time +/// tooling step, and a server runtime step. +/// +/// - parameter queryStrategy: The field execution strategy to use for query requests +/// - parameter mutationStrategy: The field execution strategy to use for mutation requests +/// - parameter subscriptionStrategy: The field execution strategy to use for subscription +/// requests +/// - parameter instrumentation: The instrumentation implementation to call during the +/// parsing, validating, execution, and field resolution stages. +/// - parameter schema: The GraphQL type system to use when validating and +/// executing a query. +/// - parameter request: A GraphQL language formatted string representing the +/// requested operation. +/// - parameter rootValue: The value provided as the first argument to resolver +/// functions on the top level type (e.g. the query object type). +/// - parameter contextValue: A context value provided to all resolver functions +/// functions +/// - parameter variableValues: A mapping of variable name to runtime value to use for all +/// variables defined in the `request`. +/// - parameter operationName: The name of the operation to use if `request` contains +/// multiple possible operations. Can be omitted if `request` contains only one operation. +/// +/// - throws: throws GraphQLError if an error occurs while parsing the `request`. +/// +/// - returns: returns a `Map` dictionary containing the result of the query inside the key +/// `data` and any validation or execution errors inside the key `errors`. The value of `data` +/// might be `null` if, for example, the query is invalid. It's possible to have both `data` and +/// `errors` if an error occurs only in a specific field. If that happens the value of that +/// field will be `null` and there will be an error inside `errors` specifying the reason for +/// the failure and the path of the failed field. +public func graphql( + queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + instrumentation: Instrumentation = NoOpInstrumentation, + schema: GraphQLSchema, + request: String, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil +) async throws -> GraphQLResult { + return try await graphql( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + request: request, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ).get() +} -#endif +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +/// This is the primary entry point function for fulfilling GraphQL subscription +/// operations by parsing, validating, and executing a GraphQL subscription +/// document along side a GraphQL schema. +/// +/// More sophisticated GraphQL servers, such as those which persist queries, +/// may wish to separate the validation and execution phases to a static time +/// tooling step, and a server runtime step. +/// +/// - parameter queryStrategy: The field execution strategy to use for query requests +/// - parameter mutationStrategy: The field execution strategy to use for mutation requests +/// - parameter subscriptionStrategy: The field execution strategy to use for subscription +/// requests +/// - parameter instrumentation: The instrumentation implementation to call during the +/// parsing, validating, execution, and field resolution stages. +/// - parameter schema: The GraphQL type system to use when validating and +/// executing a query. +/// - parameter request: A GraphQL language formatted string representing the +/// requested operation. +/// - parameter rootValue: The value provided as the first argument to resolver +/// functions on the top level type (e.g. the query object type). +/// - parameter contextValue: A context value provided to all resolver functions +/// - parameter variableValues: A mapping of variable name to runtime value to use for all +/// variables defined in the `request`. +/// - parameter operationName: The name of the operation to use if `request` contains +/// multiple possible operations. Can be omitted if `request` contains only one operation. +/// +/// - throws: throws GraphQLError if an error occurs while parsing the `request`. +/// +/// - returns: returns a SubscriptionResult containing the subscription observable inside the +/// key `observable` and any validation or execution errors inside the key `errors`. The +/// value of `observable` might be `null` if, for example, the query is invalid. It's not +/// possible to have both `observable` and `errors`. The observable payloads are +/// GraphQLResults which contain the result of the query inside the key `data` and any +/// validation or execution errors inside the key `errors`. The value of `data` might be `null`. +/// It's possible to have both `data` and `errors` if an error occurs only in a specific field. +/// If that happens the value of that field will be `null` and there +/// will be an error inside `errors` specifying the reason for the failure and the path of the +/// failed field. +public func graphqlSubscribe( + queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), + mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), + subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), + instrumentation: Instrumentation = NoOpInstrumentation, + schema: GraphQLSchema, + request: String, + rootValue: Any = (), + context: Any = (), + eventLoopGroup: EventLoopGroup, + variableValues: [String: Map] = [:], + operationName: String? = nil +) async throws -> SubscriptionResult { + return try await graphqlSubscribe( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + request: request, + rootValue: rootValue, + context: context, + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: operationName + ).get() +} diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index 54eaf102..a654ab30 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -8,75 +8,68 @@ open class EventStream { } } -#if compiler(>=5.5) && canImport(_Concurrency) +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +/// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system. +public class ConcurrentEventStream: EventStream { + public let stream: AsyncThrowingStream - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - /// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system. - public class ConcurrentEventStream: EventStream { - public let stream: AsyncThrowingStream - - public init(_ stream: AsyncThrowingStream) { - self.stream = stream - } + public init(_ stream: AsyncThrowingStream) { + self.stream = stream + } - /// Performs the closure on each event in the current stream and returns a stream of the - /// results. - /// - Parameter closure: The closure to apply to each event in the stream - /// - Returns: A stream of the results - override open func map(_ closure: @escaping (Element) throws -> To) - -> ConcurrentEventStream - { - let newStream = stream.mapStream(closure) - return ConcurrentEventStream(newStream) - } + /// Performs the closure on each event in the current stream and returns a stream of the + /// results. + /// - Parameter closure: The closure to apply to each event in the stream + /// - Returns: A stream of the results + override open func map(_ closure: @escaping (Element) throws -> To) + -> ConcurrentEventStream { + let newStream = stream.mapStream(closure) + return ConcurrentEventStream(newStream) } +} - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - extension AsyncThrowingStream { - func mapStream(_ closure: @escaping (Element) throws -> To) - -> AsyncThrowingStream - { - return AsyncThrowingStream { continuation in - let task = Task { - do { - for try await event in self { - let newEvent = try closure(event) - continuation.yield(newEvent) - } - continuation.finish() - } catch { - continuation.finish(throwing: error) +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +extension AsyncThrowingStream { + func mapStream(_ closure: @escaping (Element) throws -> To) + -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + let task = Task { + do { + for try await event in self { + let newEvent = try closure(event) + continuation.yield(newEvent) } + continuation.finish() + } catch { + continuation.finish(throwing: error) } + } - continuation.onTermination = { @Sendable reason in - task.cancel() - } + continuation.onTermination = { @Sendable reason in + task.cancel() } } + } - func filterStream(_ isIncluded: @escaping (Element) throws -> Bool) - -> AsyncThrowingStream - { - return AsyncThrowingStream { continuation in - let task = Task { - do { - for try await event in self { - if try isIncluded(event) { - continuation.yield(event) - } + func filterStream(_ isIncluded: @escaping (Element) throws -> Bool) + -> AsyncThrowingStream { + return AsyncThrowingStream { continuation in + let task = Task { + do { + for try await event in self { + if try isIncluded(event) { + continuation.yield(event) } - continuation.finish() - } catch { - continuation.finish(throwing: error) } + continuation.finish() + } catch { + continuation.finish(throwing: error) } + } - continuation.onTermination = { @Sendable _ in - task.cancel() - } + continuation.onTermination = { @Sendable _ in + task.cancel() } } } - -#endif +} diff --git a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift index a37ea671..de6252ea 100644 --- a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift @@ -63,27 +63,23 @@ class HelloWorldTests: XCTestCase { XCTAssertEqual(result, expected) } - #if compiler(>=5.5) && canImport(_Concurrency) - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - func testHelloAsync() async throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } + @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) + func testHelloAsync() async throws { + let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - let query = "{ hello }" - let expected = GraphQLResult(data: ["hello": "world"]) + defer { + XCTAssertNoThrow(try group.syncShutdownGracefully()) + } - let result = try await graphql( - schema: schema, - request: query, - eventLoopGroup: group - ) + let query = "{ hello }" + let expected = GraphQLResult(data: ["hello": "world"]) - XCTAssertEqual(result, expected) - } + let result = try await graphql( + schema: schema, + request: query, + eventLoopGroup: group + ) - #endif + XCTAssertEqual(result, expected) + } } diff --git a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index cb1ac196..21c33a86 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -1,47 +1,43 @@ import GraphQL -#if compiler(>=5.5) && canImport(_Concurrency) +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +/// A very simple publish/subscriber used for testing +class SimplePubSub { + private var subscribers: [Subscriber] - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - /// A very simple publish/subscriber used for testing - class SimplePubSub { - private var subscribers: [Subscriber] - - init() { - subscribers = [] - } - - func emit(event: T) { - for subscriber in subscribers { - subscriber.callback(event) - } - } + init() { + subscribers = [] + } - func cancel() { - for subscriber in subscribers { - subscriber.cancel() - } + func emit(event: T) { + for subscriber in subscribers { + subscriber.callback(event) } + } - func subscribe() -> ConcurrentEventStream { - let asyncStream = AsyncThrowingStream { continuation in - let subscriber = Subscriber( - callback: { newValue in - continuation.yield(newValue) - }, - cancel: { - continuation.finish() - } - ) - subscribers.append(subscriber) - } - return ConcurrentEventStream(asyncStream) + func cancel() { + for subscriber in subscribers { + subscriber.cancel() } } - struct Subscriber { - let callback: (T) -> Void - let cancel: () -> Void + func subscribe() -> ConcurrentEventStream { + let asyncStream = AsyncThrowingStream { continuation in + let subscriber = Subscriber( + callback: { newValue in + continuation.yield(newValue) + }, + cancel: { + continuation.finish() + } + ) + subscribers.append(subscriber) + } + return ConcurrentEventStream(asyncStream) } +} -#endif +struct Subscriber { + let callback: (T) -> Void + let cancel: () -> Void +} diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index b6cb4bd2..fb940c72 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -1,218 +1,214 @@ @testable import GraphQL import NIO -#if compiler(>=5.5) && canImport(_Concurrency) - - // MARK: Types - - struct Email: Encodable { - let from: String - let subject: String - let message: String - let unread: Bool - let priority: Int - - init(from: String, subject: String, message: String, unread: Bool, priority: Int = 0) { - self.from = from - self.subject = subject - self.message = message - self.unread = unread - self.priority = priority - } +// MARK: Types + +struct Email: Encodable { + let from: String + let subject: String + let message: String + let unread: Bool + let priority: Int + + init(from: String, subject: String, message: String, unread: Bool, priority: Int = 0) { + self.from = from + self.subject = subject + self.message = message + self.unread = unread + self.priority = priority } - - struct Inbox: Encodable { - let emails: [Email] +} + +struct Inbox: Encodable { + let emails: [Email] +} + +struct EmailEvent: Encodable { + let email: Email + let inbox: Inbox +} + +// MARK: Schema + +let EmailType = try! GraphQLObjectType( + name: "Email", + fields: [ + "from": GraphQLField( + type: GraphQLString + ), + "subject": GraphQLField( + type: GraphQLString + ), + "message": GraphQLField( + type: GraphQLString + ), + "unread": GraphQLField( + type: GraphQLBoolean + ), + ] +) +let InboxType = try! GraphQLObjectType( + name: "Inbox", + fields: [ + "emails": GraphQLField( + type: GraphQLList(EmailType) + ), + "total": GraphQLField( + type: GraphQLInt, + resolve: { inbox, _, _, _ in + (inbox as! Inbox).emails.count + } + ), + "unread": GraphQLField( + type: GraphQLInt, + resolve: { inbox, _, _, _ in + (inbox as! Inbox).emails.filter { $0.unread }.count + } + ), + ] +) +let EmailEventType = try! GraphQLObjectType( + name: "EmailEvent", + fields: [ + "email": GraphQLField( + type: EmailType + ), + "inbox": GraphQLField( + type: InboxType + ), + ] +) +let EmailQueryType = try! GraphQLObjectType( + name: "Query", + fields: [ + "inbox": GraphQLField( + type: InboxType + ), + ] +) + +// MARK: Test Helpers + +let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) + +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +class EmailDb { + var emails: [Email] + let publisher: SimplePubSub + + init() { + emails = [ + Email( + from: "joe@graphql.org", + subject: "Hello", + message: "Hello World", + unread: false + ), + ] + publisher = SimplePubSub() } - struct EmailEvent: Encodable { - let email: Email - let inbox: Inbox + /// Adds a new email to the database and triggers all observers + func trigger(email: Email) { + emails.append(email) + publisher.emit(event: email) } - // MARK: Schema + func stop() { + publisher.cancel() + } - let EmailType = try! GraphQLObjectType( - name: "Email", - fields: [ - "from": GraphQLField( - type: GraphQLString - ), - "subject": GraphQLField( - type: GraphQLString - ), - "message": GraphQLField( - type: GraphQLString - ), - "unread": GraphQLField( - type: GraphQLBoolean - ), - ] - ) - let InboxType = try! GraphQLObjectType( - name: "Inbox", - fields: [ - "emails": GraphQLField( - type: GraphQLList(EmailType) - ), - "total": GraphQLField( - type: GraphQLInt, - resolve: { inbox, _, _, _ in - (inbox as! Inbox).emails.count + /// Returns the default email schema, with standard resolvers. + func defaultSchema() throws -> GraphQLSchema { + return try emailSchemaWithResolvers( + resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + if let email = emailAny as? Email { + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: self.emails) + )) + } else { + throw GraphQLError(message: "\(type(of: emailAny)) is not Email") } - ), - "unread": GraphQLField( - type: GraphQLInt, - resolve: { inbox, _, _, _ in - (inbox as! Inbox).emails.filter { $0.unread }.count - } - ), - ] - ) - let EmailEventType = try! GraphQLObjectType( - name: "EmailEvent", - fields: [ - "email": GraphQLField( - type: EmailType - ), - "inbox": GraphQLField( - type: InboxType - ), - ] - ) - let EmailQueryType = try! GraphQLObjectType( - name: "Query", - fields: [ - "inbox": GraphQLField( - type: InboxType - ), - ] - ) - - // MARK: Test Helpers - - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - class EmailDb { - var emails: [Email] - let publisher: SimplePubSub - - init() { - emails = [ - Email( - from: "joe@graphql.org", - subject: "Hello", - message: "Hello World", - unread: false - ), - ] - publisher = SimplePubSub() - } - - /// Adds a new email to the database and triggers all observers - func trigger(email: Email) { - emails.append(email) - publisher.emit(event: email) - } - - func stop() { - publisher.cancel() - } - - /// Returns the default email schema, with standard resolvers. - func defaultSchema() throws -> GraphQLSchema { - return try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - if let email = emailAny as? Email { - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: self.emails) - )) - } else { - throw GraphQLError(message: "\(type(of: emailAny)) is not Email") - } - }, - subscribe: { _, args, _, eventLoopGroup, _ throws -> EventLoopFuture in - let priority = args["priority"].int ?? 0 - let filtered = self.publisher.subscribe().stream - .filterStream { emailAny throws in - if let email = emailAny as? Email { - return email.priority >= priority - } else { - return true - } + }, + subscribe: { _, args, _, eventLoopGroup, _ throws -> EventLoopFuture in + let priority = args["priority"].int ?? 0 + let filtered = self.publisher.subscribe().stream + .filterStream { emailAny throws in + if let email = emailAny as? Email { + return email.priority >= priority + } else { + return true } - return eventLoopGroup.next() - .makeSucceededFuture(ConcurrentEventStream(filtered)) - } - ) - } - - /// Generates a subscription to the database using the default schema and resolvers - func subscription( - query: String, - variableValues: [String: Map] = [:] - ) throws -> SubscriptionEventStream { - return try createSubscription( - schema: defaultSchema(), - query: query, - variableValues: variableValues - ) - } - } - - /// Generates an email schema with the specified resolve and subscribe methods - func emailSchemaWithResolvers( - resolve: GraphQLFieldResolve? = nil, - subscribe: GraphQLFieldResolve? = nil - ) throws -> GraphQLSchema { - return try GraphQLSchema( - query: EmailQueryType, - subscription: try! GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ), - ], - resolve: resolve, - subscribe: subscribe - ), - ] - ) + } + return eventLoopGroup.next() + .makeSucceededFuture(ConcurrentEventStream(filtered)) + } ) } - /// Generates a subscription from the given schema and query. It's expected that the - /// resolver/database interactions are configured by the caller. - func createSubscription( - schema: GraphQLSchema, + /// Generates a subscription to the database using the default schema and resolvers + func subscription( query: String, variableValues: [String: Map] = [:] ) throws -> SubscriptionEventStream { - let result = try graphqlSubscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, - schema: schema, - request: query, - rootValue: (), - context: (), - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: nil - ).wait() - - if let stream = result.stream { - return stream - } else { - throw result.errors.first! // We may have more than one... - } + return try createSubscription( + schema: defaultSchema(), + query: query, + variableValues: variableValues + ) } - -#endif +} + +/// Generates an email schema with the specified resolve and subscribe methods +func emailSchemaWithResolvers( + resolve: GraphQLFieldResolve? = nil, + subscribe: GraphQLFieldResolve? = nil +) throws -> GraphQLSchema { + return try GraphQLSchema( + query: EmailQueryType, + subscription: try! GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ), + ], + resolve: resolve, + subscribe: subscribe + ), + ] + ) + ) +} + +/// Generates a subscription from the given schema and query. It's expected that the +/// resolver/database interactions are configured by the caller. +func createSubscription( + schema: GraphQLSchema, + query: String, + variableValues: [String: Map] = [:] +) throws -> SubscriptionEventStream { + let result = try graphqlSubscribe( + queryStrategy: SerialFieldExecutionStrategy(), + mutationStrategy: SerialFieldExecutionStrategy(), + subscriptionStrategy: SerialFieldExecutionStrategy(), + instrumentation: NoOpInstrumentation, + schema: schema, + request: query, + rootValue: (), + context: (), + eventLoopGroup: eventLoopGroup, + variableValues: variableValues, + operationName: nil + ).wait() + + if let stream = result.stream { + return stream + } else { + throw result.errors.first! // We may have more than one... + } +} diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index 5ae2e781..955a5a9b 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -2,301 +2,324 @@ import GraphQL import NIO import XCTest -#if compiler(>=5.5) && canImport(_Concurrency) - - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) - /// This follows the graphql-js testing, with deviations where noted. - class SubscriptionTests: XCTestCase { - let timeoutDuration = 0.5 // in seconds - - // MARK: Test primary graphqlSubscribe function - - /// This test is not present in graphql-js, but just tests basic functionality. - func testGraphqlSubscribe() async throws { - let db = EmailDb() - let schema = try db.defaultSchema() - let query = """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } +@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) +/// This follows the graphql-js testing, with deviations where noted. +class SubscriptionTests: XCTestCase { + let timeoutDuration = 0.5 // in seconds + + // MARK: Test primary graphqlSubscribe function + + /// This test is not present in graphql-js, but just tests basic functionality. + func testGraphqlSubscribe() async throws { + let db = EmailDb() + let schema = try db.defaultSchema() + let query = """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject } - """ - - let subscriptionResult = try graphqlSubscribe( - schema: schema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() - 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() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - db.stop() - let result = try await iterator.next()?.get() - XCTAssertEqual( - result, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", + inbox { + unread + total + } + } + } + """ + + let subscriptionResult = try graphqlSubscribe( + schema: schema, + request: query, + eventLoopGroup: eventLoopGroup + ).wait() + 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() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + db.stop() + let result = try await iterator.next()?.get() + XCTAssertEqual( + result, + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 1, + "total": 2, + ], + ]] + ) + ) + } + + // MARK: Subscription Initialization Phase + + /// accepts multiple subscription fields defined in schema + func testAcceptsMultipleSubscriptionFields() async throws { + let db = EmailDb() + let schema = try GraphQLSchema( + query: EmailQueryType, + subscription: GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ), ], - "inbox": [ - "unread": 1, - "total": 2, + resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< + Any? + > in + guard let email = emailAny as? Email else { + throw GraphQLError( + message: "Source is not Email type: \(type(of: emailAny))" + ) + } + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: db.emails) + )) + }, + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< + Any? + > in + eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + } + ), + "notImportantEmail": GraphQLField( + type: EmailEventType, + args: [ + "priority": GraphQLArgument( + type: GraphQLInt + ), ], - ]] - ) + resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< + Any? + > in + guard let email = emailAny as? Email else { + throw GraphQLError( + message: "Source is not Email type: \(type(of: emailAny))" + ) + } + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: db.emails) + )) + }, + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< + Any? + > in + eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + } + ), + ] ) + ) + let subscription = try createSubscription(schema: schema, query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return } + var iterator = stream.stream.makeAsyncIterator() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + let result = try await iterator.next()?.get() + XCTAssertEqual( + result, + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 1, + "total": 2, + ], + ]] + ) + ) + } - // MARK: Subscription Initialization Phase - - /// accepts multiple subscription fields defined in schema - func testAcceptsMultipleSubscriptionFields() async throws { - let db = EmailDb() - let schema = try GraphQLSchema( - query: EmailQueryType, - subscription: GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ), - ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - guard let email = emailAny as? Email else { - throw GraphQLError( - message: "Source is not Email type: \(type(of: emailAny))" - ) - } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) - } - ), - "notImportantEmail": GraphQLField( - type: EmailEventType, - args: [ - "priority": GraphQLArgument( - type: GraphQLInt - ), - ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - guard let email = emailAny as? Email else { - throw GraphQLError( - message: "Source is not Email type: \(type(of: emailAny))" - ) - } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) - } - ), - ] - ) + /// 'should only resolve the first field of invalid multi-field' + /// + /// Note that due to implementation details in Swift, this will not resolve the "first" one, + /// but rather a random one of the two + func testInvalidMultiField() async throws { + let db = EmailDb() + + var didResolveImportantEmail = false + var didResolveNonImportantEmail = false + + let schema = try GraphQLSchema( + query: EmailQueryType, + subscription: GraphQLObjectType( + name: "Subscription", + fields: [ + "importantEmail": GraphQLField( + type: EmailEventType, + resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< + Any? + > in + didResolveImportantEmail = true + return eventLoopGroup.next() + .makeSucceededFuture(db.publisher.subscribe()) + } + ), + "notImportantEmail": GraphQLField( + type: EmailEventType, + resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< + Any? + > in + didResolveNonImportantEmail = true + return eventLoopGroup.next() + .makeSucceededFuture(db.publisher.subscribe()) + } + ), + ] ) - let subscription = try createSubscription(schema: schema, query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { + ) + let _ = try createSubscription(schema: schema, query: """ + subscription { + importantEmail { + email { from - subject - } - inbox { - unread - total - } } - } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return + } + notImportantEmail { + email { + from + } + } } - var iterator = stream.stream.makeAsyncIterator() + """) + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + // One and only one should be true + XCTAssertTrue(didResolveImportantEmail || didResolveNonImportantEmail) + XCTAssertFalse(didResolveImportantEmail && didResolveNonImportantEmail) + } + + // 'throws an error if schema is missing' + // Not implemented because this is taken care of by Swift optional types - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) + // 'throws an error if document is missing' + // Not implemented because this is taken care of by Swift optional types - let result = try await iterator.next()?.get() + /// 'resolves to an error for unknown subscription field' + func testErrorUnknownSubscriptionField() throws { + let db = EmailDb() + XCTAssertThrowsError( + try db.subscription(query: """ + subscription { + unknownField + } + """) + ) { error in + guard let graphQLError = error as? GraphQLError else { + XCTFail("Error was not of type GraphQLError") + return + } XCTAssertEqual( - result, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", - ], - "inbox": [ - "unread": 1, - "total": 2, - ], - ]] - ) + graphQLError.message, + "Cannot query field \"unknownField\" on type \"Subscription\"." ) + XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) } + } - /// 'should only resolve the first field of invalid multi-field' - /// - /// Note that due to implementation details in Swift, this will not resolve the "first" one, - /// but rather a random one of the two - func testInvalidMultiField() async throws { - let db = EmailDb() - - var didResolveImportantEmail = false - var didResolveNonImportantEmail = false - - let schema = try GraphQLSchema( - query: EmailQueryType, - subscription: GraphQLObjectType( - name: "Subscription", - fields: [ - "importantEmail": GraphQLField( - type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - didResolveImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) - } - ), - "notImportantEmail": GraphQLField( - type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - didResolveNonImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) - } - ), - ] - ) - ) - let _ = try createSubscription(schema: schema, query: """ + /// 'should pass through unexpected errors thrown in subscribe' + func testPassUnexpectedSubscribeErrors() throws { + let db = EmailDb() + XCTAssertThrowsError( + try db.subscription(query: "") + ) + } + + /// 'throws an error if subscribe does not return an iterator' + func testErrorIfSubscribeIsntIterator() throws { + let schema = try emailSchemaWithResolvers( + resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeSucceededFuture(nil) + }, + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeSucceededFuture("test") + } + ) + XCTAssertThrowsError( + try createSubscription(schema: schema, query: """ subscription { importantEmail { email { from } } - notImportantEmail { - email { - from - } - } } """) - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - - // One and only one should be true - XCTAssertTrue(didResolveImportantEmail || didResolveNonImportantEmail) - XCTAssertFalse(didResolveImportantEmail && didResolveNonImportantEmail) - } - - // 'throws an error if schema is missing' - // Not implemented because this is taken care of by Swift optional types - - // 'throws an error if document is missing' - // Not implemented because this is taken care of by Swift optional types - - /// 'resolves to an error for unknown subscription field' - func testErrorUnknownSubscriptionField() throws { - let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: """ - subscription { - unknownField - } - """) - ) { error in - guard let graphQLError = error as? GraphQLError else { - XCTFail("Error was not of type GraphQLError") - return - } - XCTAssertEqual( - graphQLError.message, - "Cannot query field \"unknownField\" on type \"Subscription\"." - ) - XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) + ) { error in + guard let graphQLError = error as? GraphQLError else { + XCTFail("Error was not of type GraphQLError") + return } - } - - /// 'should pass through unexpected errors thrown in subscribe' - func testPassUnexpectedSubscribeErrors() throws { - let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: "") + XCTAssertEqual( + graphQLError.message, + "Subscription field resolver must return EventStream. Received: 'test'" ) } + } - /// 'throws an error if subscribe does not return an iterator' - func testErrorIfSubscribeIsntIterator() throws { - let schema = try emailSchemaWithResolvers( - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) - }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture("test") - } - ) + /// 'resolves to an error for subscription resolver errors' + func testErrorForSubscriptionResolverErrors() throws { + func verifyError(schema: GraphQLSchema) { XCTAssertThrowsError( try createSubscription(schema: schema, query: """ subscription { @@ -312,203 +335,109 @@ import XCTest XCTFail("Error was not of type GraphQLError") return } - XCTAssertEqual( - graphQLError.message, - "Subscription field resolver must return EventStream. Received: 'test'" - ) + XCTAssertEqual(graphQLError.message, "test error") } } - /// 'resolves to an error for subscription resolver errors' - func testErrorForSubscriptionResolverErrors() throws { - func verifyError(schema: GraphQLSchema) { - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ - subscription { - importantEmail { - email { - from - } - } - } - """) - ) { error in - guard let graphQLError = error as? GraphQLError else { - XCTFail("Error was not of type GraphQLError") - return - } - XCTAssertEqual(graphQLError.message, "test error") - } + // Throwing an error + try verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _, _ throws -> EventLoopFuture in + throw GraphQLError(message: "test error") } + )) - // Throwing an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _, _ throws -> EventLoopFuture in - throw GraphQLError(message: "test error") - } - )) - - // Resolving to an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) - } - )) - - // Rejecting with an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeFailedFuture(GraphQLError(message: "test error")) - } - )) - } - - /// 'resolves to an error for source event stream resolver errors' - // Tests above cover this - - /// 'resolves to an error if variables were wrong type' - func testErrorVariablesWrongType() throws { - let db = EmailDb() - let query = """ - subscription ($priority: Int) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """ - - XCTAssertThrowsError( - try db.subscription( - query: query, - variableValues: [ - "priority": "meow", - ] - ) - ) { error in - guard let graphQLError = error as? GraphQLError else { - XCTFail("Error was not of type GraphQLError") - return - } - XCTAssertEqual( - graphQLError.message, - "Variable \"$priority\" got invalid value \"\"meow\"\".\nExpected type \"Int\", found \"meow\"." - ) + // Resolving to an error + try verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeSucceededFuture(GraphQLError(message: "test error")) } - } + )) - // MARK: Subscription Publish Phase + // Rejecting with an error + try verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeFailedFuture(GraphQLError(message: "test error")) + } + )) + } - /// 'produces a payload for a single subscriber' - func testSingleSubscriber() async throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + /// 'resolves to an error for source event stream resolver errors' + // Tests above cover this + + /// 'resolves to an error if variables were wrong type' + func testErrorVariablesWrongType() throws { + let db = EmailDb() + let query = """ + subscription ($priority: Int) { + importantEmail(priority: $priority) { + email { + from + subject } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") + inbox { + unread + total + } + } + } + """ + + XCTAssertThrowsError( + try db.subscription( + query: query, + variableValues: [ + "priority": "meow", + ] + ) + ) { error in + guard let graphQLError = error as? GraphQLError else { + XCTFail("Error was not of type GraphQLError") return } - var iterator = stream.stream.makeAsyncIterator() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - db.stop() - - let result = try await iterator.next()?.get() XCTAssertEqual( - result, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", - ], - "inbox": [ - "unread": 1, - "total": 2, - ], - ]] - ) + graphQLError.message, + "Variable \"$priority\" got invalid value \"\"meow\"\".\nExpected type \"Int\", found \"meow\"." ) } + } - /// 'produces a payload for multiple subscribe in same subscription' - func testMultipleSubscribers() async throws { - let db = EmailDb() - let subscription1 = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + // MARK: Subscription Publish Phase + + /// 'produces a payload for a single subscriber' + func testSingleSubscriber() async throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject } - """) - guard let stream1 = subscription1 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - - let subscription2 = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + inbox { + unread + total } - """) - guard let stream2 = subscription2 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - - var iterator1 = stream1.stream.makeAsyncIterator() - var iterator2 = stream2.stream.makeAsyncIterator() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - - let result1 = try await iterator1.next()?.get() - let result2 = try await iterator2.next()?.get() - - let expected = GraphQLResult( + } + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return + } + var iterator = stream.stream.makeAsyncIterator() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + db.stop() + + let result = try await iterator.next()?.get() + XCTAssertEqual( + result, + GraphQLResult( data: ["importantEmail": [ "email": [ "from": "yuzhi@graphql.org", @@ -520,491 +449,559 @@ import XCTest ], ]] ) + ) + } - XCTAssertEqual(result1, expected) - XCTAssertEqual(result2, expected) + /// 'produces a payload for multiple subscribe in same subscription' + func testMultipleSubscribers() async throws { + let db = EmailDb() + let subscription1 = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + guard let stream1 = subscription1 as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return } - /// 'produces a payload per subscription event' - func testPayloadPerEvent() async throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + let subscription2 = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() - - // A new email arrives! - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - let result1 = try await iterator.next()?.get() - XCTAssertEqual( - result1, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", - ], - "inbox": [ - "unread": 1, - "total": 2, - ], - ]] - ) - ) + inbox { + unread + total + } + } + } + """) + guard let stream2 = subscription2 as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return + } - // Another new email arrives - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Tools", - message: "I <3 making things", - unread: true - )) - let result2 = try await iterator.next()?.get() - XCTAssertEqual( - result2, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "hyo@graphql.org", - "subject": "Tools", - ], - "inbox": [ - "unread": 2, - "total": 3, - ], - ]] - ) - ) + var iterator1 = stream1.stream.makeAsyncIterator() + var iterator2 = stream2.stream.makeAsyncIterator() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + + let result1 = try await iterator1.next()?.get() + let result2 = try await iterator2.next()?.get() + + let expected = GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 1, + "total": 2, + ], + ]] + ) + + XCTAssertEqual(result1, expected) + XCTAssertEqual(result2, expected) + } + + /// 'produces a payload per subscription event' + func testPayloadPerEvent() async throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total + } + } + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return } + var iterator = stream.stream.makeAsyncIterator() + + // A new email arrives! + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + let result1 = try await iterator.next()?.get() + XCTAssertEqual( + result1, + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 1, + "total": 2, + ], + ]] + ) + ) + + // Another new email arrives + db.trigger(email: Email( + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true + )) + let result2 = try await iterator.next()?.get() + XCTAssertEqual( + result2, + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "hyo@graphql.org", + "subject": "Tools", + ], + "inbox": [ + "unread": 2, + "total": 3, + ], + ]] + ) + ) + } - /// Tests that subscriptions use arguments correctly. - /// This is not in the graphql-js tests. - func testArguments() async throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 5) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + /// Tests that subscriptions use arguments correctly. + /// This is not in the graphql-js tests. + func testArguments() async throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 5) { + importantEmail(priority: $priority) { + email { + from + subject } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } + inbox { + unread + total + } + } + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return + } - var results = [GraphQLResult]() - var expectation = XCTestExpectation() + var results = [GraphQLResult]() + var expectation = XCTestExpectation() - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - let keepForNow = stream.map { event in - event.map { result in - results.append(result) - expectation.fulfill() - } + // So that the Task won't immediately be cancelled since the ConcurrentEventStream is + // discarded + let keepForNow = stream.map { event in + event.map { result in + results.append(result) + expectation.fulfill() } + } - var expected = [GraphQLResult]() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true, - priority: 7 - )) - expected.append( - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", - ], - "inbox": [ - "unread": 1, - "total": 2, - ], - ]] - ) + var expected = [GraphQLResult]() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true, + priority: 7 + )) + expected.append( + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 1, + "total": 2, + ], + ]] ) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - // Low priority email shouldn't trigger an event - expectation = XCTestExpectation() - expectation.isInverted = true - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Not Important", - message: "Ignore this email", - unread: true, - priority: 2 - )) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - // Higher priority one should trigger again - expectation = XCTestExpectation() - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Tools", - message: "I <3 making things", - unread: true, - priority: 5 - )) - expected.append( - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "hyo@graphql.org", - "subject": "Tools", - ], - "inbox": [ - "unread": 3, - "total": 4, - ], - ]] - ) + ) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) + + // Low priority email shouldn't trigger an event + expectation = XCTestExpectation() + expectation.isInverted = true + db.trigger(email: Email( + from: "hyo@graphql.org", + subject: "Not Important", + message: "Ignore this email", + unread: true, + priority: 2 + )) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) + + // Higher priority one should trigger again + expectation = XCTestExpectation() + db.trigger(email: Email( + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true, + priority: 5 + )) + expected.append( + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "hyo@graphql.org", + "subject": "Tools", + ], + "inbox": [ + "unread": 3, + "total": 4, + ], + ]] ) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) + ) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow - } + // So that the Task won't immediately be cancelled since the ConcurrentEventStream is + // discarded + _ = keepForNow + } - /// 'should not trigger when subscription is already done' - func testNoTriggerAfterDone() async throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + /// 'should not trigger when subscription is already done' + func testNoTriggerAfterDone() async throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject + } + inbox { + unread + total } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - - var results = [GraphQLResult]() - var expectation = XCTestExpectation() - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - let keepForNow = stream.map { event in - event.map { result in - results.append(result) - expectation.fulfill() } - } - var expected = [GraphQLResult]() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - expected.append( - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", - ], - "inbox": [ - "unread": 1, - "total": 2, - ], - ]] - ) - ) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - db.stop() - - // This should not trigger an event. - expectation = XCTestExpectation() - expectation.isInverted = true - db.trigger(email: Email( - from: "hyo@graphql.org", - subject: "Tools", - message: "I <3 making things", - unread: true - )) - - // Ensure that the current result was the one before the db was stopped - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return } - /// 'should not trigger when subscription is thrown' - // Not necessary - Swift async stream handles throwing errors + var results = [GraphQLResult]() + var expectation = XCTestExpectation() + // So that the Task won't immediately be cancelled since the ConcurrentEventStream is + // discarded + let keepForNow = stream.map { event in + event.map { result in + results.append(result) + expectation.fulfill() + } + } + var expected = [GraphQLResult]() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + expected.append( + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 1, + "total": 2, + ], + ]] + ) + ) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) + + db.stop() + + // This should not trigger an event. + expectation = XCTestExpectation() + expectation.isInverted = true + db.trigger(email: Email( + from: "hyo@graphql.org", + subject: "Tools", + message: "I <3 making things", + unread: true + )) + + // Ensure that the current result was the one before the db was stopped + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) + + // So that the Task won't immediately be cancelled since the ConcurrentEventStream is + // discarded + _ = keepForNow + } - /// 'event order is correct for multiple publishes' - func testOrderCorrectForMultiplePublishes() async throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + /// 'should not trigger when subscription is thrown' + // Not necessary - Swift async stream handles throwing errors + + /// 'event order is correct for multiple publishes' + func testOrderCorrectForMultiplePublishes() async throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Alright", - message: "Tests are good", - unread: true - )) - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Message 2", - message: "Tests are good 2", - unread: true - )) - - let result1 = try await iterator.next()?.get() - XCTAssertEqual( - result1, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Alright", - ], - "inbox": [ - "unread": 2, - "total": 3, - ], - ]] - ) + inbox { + unread + total + } + } + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return + } + var iterator = stream.stream.makeAsyncIterator() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Alright", + message: "Tests are good", + unread: true + )) + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Message 2", + message: "Tests are good 2", + unread: true + )) + + let result1 = try await iterator.next()?.get() + XCTAssertEqual( + result1, + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Alright", + ], + "inbox": [ + "unread": 2, + "total": 3, + ], + ]] ) + ) - let result2 = try await iterator.next()?.get() - XCTAssertEqual( - result2, - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "from": "yuzhi@graphql.org", - "subject": "Message 2", - ], - "inbox": [ - "unread": 2, - "total": 3, - ], - ]] - ) + let result2 = try await iterator.next()?.get() + XCTAssertEqual( + result2, + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "from": "yuzhi@graphql.org", + "subject": "Message 2", + ], + "inbox": [ + "unread": 2, + "total": 3, + ], + ]] ) - } + ) + } - /// 'should handle error during execution of source event' - func testErrorDuringSubscription() async throws { - let db = EmailDb() + /// 'should handle error during execution of source event' + func testErrorDuringSubscription() async throws { + let db = EmailDb() - let schema = try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - guard let email = emailAny as? Email else { - throw GraphQLError( - message: "Source is not Email type: \(type(of: emailAny))" - ) - } - if email.subject == "Goodbye" { // Force the system to fail here. - throw GraphQLError(message: "Never leave.") - } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( - email: email, - inbox: Inbox(emails: db.emails) - )) - }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + let schema = try emailSchemaWithResolvers( + resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + guard let email = emailAny as? Email else { + throw GraphQLError( + message: "Source is not Email type: \(type(of: emailAny))" + ) } - ) + if email.subject == "Goodbye" { // Force the system to fail here. + throw GraphQLError(message: "Never leave.") + } + return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + email: email, + inbox: Inbox(emails: db.emails) + )) + }, + subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + } + ) - let subscription = try createSubscription(schema: schema, query: """ - subscription { - importantEmail { - email { - subject - } + let subscription = try createSubscription(schema: schema, query: """ + subscription { + importantEmail { + email { + subject } } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return + } - var results = [GraphQLResult]() - var expectation = XCTestExpectation() - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - let keepForNow = stream.map { event in - event.map { result in - results.append(result) - expectation.fulfill() - } + var results = [GraphQLResult]() + var expectation = XCTestExpectation() + // So that the Task won't immediately be cancelled since the ConcurrentEventStream is + // discarded + let keepForNow = stream.map { event in + event.map { result in + results.append(result) + expectation.fulfill() } - var expected = [GraphQLResult]() - - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Hello", - message: "Tests are good", - unread: true - )) - expected.append( - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "subject": "Hello", - ], - ]] - ) + } + var expected = [GraphQLResult]() + + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Hello", + message: "Tests are good", + unread: true + )) + expected.append( + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "subject": "Hello", + ], + ]] ) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - expectation = XCTestExpectation() - // An error in execution is presented as such. - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Goodbye", - message: "Tests are good", - unread: true - )) - expected.append( - GraphQLResult( - data: ["importantEmail": nil], - errors: [ - GraphQLError(message: "Never leave."), - ] - ) + ) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) + + expectation = XCTestExpectation() + // An error in execution is presented as such. + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Goodbye", + message: "Tests are good", + unread: true + )) + expected.append( + GraphQLResult( + data: ["importantEmail": nil], + errors: [ + GraphQLError(message: "Never leave."), + ] ) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - expectation = XCTestExpectation() - // However that does not close the response event stream. Subsequent events are still - // executed. - db.trigger(email: Email( - from: "yuzhi@graphql.org", - subject: "Bonjour", - message: "Tests are good", - unread: true - )) - expected.append( - GraphQLResult( - data: ["importantEmail": [ - "email": [ - "subject": "Bonjour", - ], - ]] - ) + ) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) + + expectation = XCTestExpectation() + // However that does not close the response event stream. Subsequent events are still + // executed. + db.trigger(email: Email( + from: "yuzhi@graphql.org", + subject: "Bonjour", + message: "Tests are good", + unread: true + )) + expected.append( + GraphQLResult( + data: ["importantEmail": [ + "email": [ + "subject": "Bonjour", + ], + ]] ) - wait(for: [expectation], timeout: timeoutDuration) - XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow - } + ) + wait(for: [expectation], timeout: timeoutDuration) + XCTAssertEqual(results, expected) - /// 'should pass through error thrown in source event stream' - // Handled by AsyncThrowingStream + // So that the Task won't immediately be cancelled since the ConcurrentEventStream is + // discarded + _ = keepForNow + } - /// Test incorrect emitted type errors - func testErrorWrongEmitType() async throws { - let db = EmailDb() - let subscription = try db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } + /// 'should pass through error thrown in source event stream' + // Handled by AsyncThrowingStream + + /// Test incorrect emitted type errors + func testErrorWrongEmitType() async throws { + let db = EmailDb() + let subscription = try db.subscription(query: """ + subscription ($priority: Int = 0) { + importantEmail(priority: $priority) { + email { + from + subject } - """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() - - db.publisher.emit(event: "String instead of email") - - let result = try await iterator.next()?.get() - XCTAssertEqual( - result, - GraphQLResult( - data: ["importantEmail": nil], - errors: [ - GraphQLError(message: "String is not Email"), - ] - ) - ) + inbox { + unread + total + } + } + } + """) + guard let stream = subscription as? ConcurrentEventStream else { + XCTFail("stream isn't ConcurrentEventStream") + return } + var iterator = stream.stream.makeAsyncIterator() + + db.publisher.emit(event: "String instead of email") + + let result = try await iterator.next()?.get() + XCTAssertEqual( + result, + GraphQLResult( + data: ["importantEmail": nil], + errors: [ + GraphQLError(message: "String is not Email"), + ] + ) + ) } -#endif +} From ed2a9967775139ca550f3a20a3992b9059525af2 Mon Sep 17 00:00:00 2001 From: Petro Rovenskyi Date: Fri, 18 Oct 2024 15:25:14 +0300 Subject: [PATCH 2/2] updated README with Swift Support section --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index baef8683..e4698313 100644 --- a/README.md +++ b/README.md @@ -119,6 +119,10 @@ If you encode a `GraphQLResult` with an ordinary `JSONEncoder`, there are no gua violating the [GraphQL spec](https://spec.graphql.org/June2018/#sec-Serialized-Map-Ordering). To preserve this order, `GraphQLResult` should be encoded using the `GraphQLJSONEncoder` provided by this package. +## Support + +This package supports Swift versions in [alignment with Swift NIO](https://github.com/apple/swift-nio?tab=readme-ov-file#swift-versions). + ## Contributing If you think you have found a security vulnerability, please follow the