diff --git a/MIGRATION.md b/MIGRATION.md index b5c3d3ce..3eda163e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,62 @@ # Migration -## 2.0 to 3.0 +## 3 to 4 + +### NIO removal + +All NIO-based arguments and return types were removed, including all `EventLoopGroup` and `EventLoopFuture` parameters. + +As such, all `execute` and `subscribe` calls should have the `eventLoopGroup` argument removed, and the `await` keyword should be used. + +Also, all resolver closures must remove the `eventLoopGroup` argument, and all that return an `EventLoopFuture` should be converted to an `async` function. + +The documentation here will be very helpful in the conversion: https://www.swift.org/documentation/server/guides/libraries/concurrency-adoption-guidelines.html + +### Swift Concurrency checking + +With the conversion from NIO to Swift Concurrency, types used across async boundaries should conform to `Sendable` to avoid errors and warnings. This includes the Swift types and functions that back the GraphQL schema. For more details on the conversion, see the [Sendable documentation](https://developer.apple.com/documentation/swift/sendable). + +### `ExecutionStrategy` argument removals + +The `queryStrategy`, `mutationStrategy`, and `subscriptionStrategy` arguments have been removed from `graphql` and `graphqlSubscribe`. Instead Queries and Subscriptions are executed in parallel and Mutations are executed serially, [as required by the spec](https://spec.graphql.org/October2021/#sec-Mutation). + +### `validationRules` argument reorder + +The `validationRules` argument has been moved from the beginning of `graphql` and `graphqlSubscribe` to the end to better reflect its relative importance: + + +```swift +// Before +let result = try await graphql( + validationRules: [ruleABC], + schema: schema, + ... +) +// After +let result = try await graphql( + schema: schema, + ... + validationRules: [ruleABC] +) +``` + +### EventStream removal + +The `EventStream` abstraction used to provide pre-concurrency subscription support has been removed. This means that `graphqlSubscribe(...).stream` will now be an `AsyncThrowingStream` type, instead of an `EventStream` type, and that downcasting to `ConcurrentEventStream` is no longer necessary. + +### SubscriptionResult removal + +The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns `Result, GraphQLErrors>`. + +### Instrumentation removal + +The `Instrumentation` type has been removed, with anticipated support for tracing using [`swift-distributed-tracing`](https://github.com/apple/swift-distributed-tracing). `instrumentation` arguments must be removed from `graphql` and `graphqlSubscribe` calls. + +### AST Node `set` + +The deprecated `Node.set(value: Node?, key: String)` function was removed in preference of the `Node.set(value _: NodeResult?, key _: String)`. Change any calls from `node.set(value: node, key: string)` to `node.set(.node(node), string)`. + +## 2 to 3 ### TypeReference removal @@ -73,4 +129,4 @@ The following type properties were changed from arrays to closures. To get the a ### GraphQL type codability -With GraphQL type definitions now including closures, many of the objects in [Definition](https://github.com/GraphQLSwift/GraphQL/blob/main/Sources/GraphQL/Type/Definition.swift) are no longer codable. If you are depending on codability, you can conform the type appropriately in your downstream package. \ No newline at end of file +With GraphQL type definitions now including closures, many of the objects in [Definition](https://github.com/GraphQLSwift/GraphQL/blob/main/Sources/GraphQL/Type/Definition.swift) are no longer codable. If you are depending on codability, you can conform the type appropriately in your downstream package. diff --git a/Package.resolved b/Package.resolved index d4fd520f..a9ec17d4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,14 +1,5 @@ { "pins" : [ - { - "identity" : "swift-atomics", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-atomics.git", - "state" : { - "revision" : "cd142fd2f64be2100422d658e7411e39489da985", - "version" : "1.2.0" - } - }, { "identity" : "swift-collections", "kind" : "remoteSourceControl", @@ -17,24 +8,6 @@ "revision" : "671108c96644956dddcd89dd59c203dcdb36cec7", "version" : "1.1.4" } - }, - { - "identity" : "swift-nio", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-nio.git", - "state" : { - "revision" : "27c839f4700069928196cd0e9fa03b22f297078a", - "version" : "2.78.0" - } - }, - { - "identity" : "swift-system", - "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-system.git", - "state" : { - "revision" : "c8a44d836fe7913603e246acab7c528c2e780168", - "version" : "1.4.0" - } } ], "version" : 2 diff --git a/Package.swift b/Package.swift index 2838065f..bcc14305 100644 --- a/Package.swift +++ b/Package.swift @@ -3,18 +3,17 @@ import PackageDescription let package = Package( name: "GraphQL", + platforms: [.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)], products: [ .library(name: "GraphQL", targets: ["GraphQL"]), ], dependencies: [ - .package(url: "https://github.com/apple/swift-nio.git", .upToNextMajor(from: "2.10.1")), .package(url: "https://github.com/apple/swift-collections", .upToNextMajor(from: "1.0.0")), ], targets: [ .target( name: "GraphQL", dependencies: [ - .product(name: "NIO", package: "swift-nio"), .product(name: "OrderedCollections", package: "swift-collections"), ] ), @@ -26,5 +25,6 @@ let package = Package( .copy("LanguageTests/schema-kitchen-sink.graphql"), ] ), - ] + ], + swiftLanguageVersions: [.v5, .version("6")] ) diff --git a/README.md b/README.md index 87aeef25..284de788 100644 --- a/README.md +++ b/README.md @@ -45,8 +45,7 @@ Once a schema has been defined queries may be executed against it using the glob ```swift let result = try await graphql( schema: schema, - request: "{ hello }", - eventLoopGroup: eventLoopGroup + request: "{ hello }" ) ``` @@ -58,33 +57,29 @@ The result of this query is a `GraphQLResult` that encodes to the following JSON ### Subscription -This package supports GraphQL subscription, but until the integration of `AsyncSequence` in Swift 5.5 the standard Swift library did not -provide an event-stream construct. For historical reasons and backwards compatibility, this library implements subscriptions using an -`EventStream` protocol that nearly every asynchronous stream implementation can conform to. - -To create a subscription field in a GraphQL schema, use the `subscribe` resolver that returns an `EventStream`. You must also provide a -`resolver`, which defines how to process each event as it occurs and must return the field result type. Here is an example: +This package supports GraphQL subscription. To create a subscription field in a GraphQL schema, use the `subscribe` +resolver that returns any type that conforms to `AsyncSequence`. You must also provide a `resolver`, which defines how +to process each event as it occurs and must return the field result type. Here is an example: ```swift let schema = try GraphQLSchema( subscribe: GraphQLObjectType( name: "Subscribe", fields: [ - "hello": GraphQLField( + "hello": GraphQLField( type: GraphQLString, - resolve: { eventResult, _, _, _, _ in // Defines how to transform each event when it occurs + resolve: { eventResult, _, _, _ in // Defines how to transform each event when it occurs return eventResult }, - subscribe: { _, _, _, _, _ in // Defines how to construct the event stream - let asyncStream = AsyncThrowingStream { continuation in + subscribe: { _, _, _, _ in // Defines how to construct the event stream + return AsyncThrowingStream { continuation in let timer = Timer.scheduledTimer( withTimeInterval: 3, repeats: true, ) { - continuation.yield("world") // Emits "world" every 3 seconds + continuation.yield("world") // Emits "world" every 3 seconds } } - return ConcurrentEventStream(asyncStream) } ) ] @@ -98,9 +93,8 @@ To execute a subscription use the `graphqlSubscribe` function: let subscriptionResult = try await graphqlSubscribe( schema: schema, ) -// Must downcast from EventStream to concrete type to use in 'for await' loop below -let concurrentStream = subscriptionResult.stream! as! ConcurrentEventStream -for try await result in concurrentStream.stream { +let stream = subscriptionResult.get() +for try await result in stream { print(result) } ``` @@ -111,18 +105,15 @@ The code above will print the following JSON every 3 seconds: { "hello": "world" } ``` -The example above assumes that your environment has access to Swift Concurrency. If that is not the case, try using -[GraphQLRxSwift](https://github.com/GraphQLSwift/GraphQLRxSwift) - ## Encoding Results -If you encode a `GraphQLResult` with an ordinary `JSONEncoder`, there are no guarantees that the field order will match the query, +If you encode a `GraphQLResult` with an ordinary `JSONEncoder`, there are no guarantees that the field order will match the query, 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). +This package aims to support the previous three Swift versions. For details on upgrading to new major versions, see [MIGRATION](MIGRATION.md). @@ -140,7 +131,7 @@ To format your code, install `swiftformat` and run: ```bash swiftformat . -``` +``` Most of this repo mirrors the structure of (the canonical GraphQL implementation written in Javascript/Typescript)[https://github.com/graphql/graphql-js]. If there is any feature diff --git a/Sources/GraphQL/Error/GraphQLError.swift b/Sources/GraphQL/Error/GraphQLError.swift index fd8876a3..0ddb4d3b 100644 --- a/Sources/GraphQL/Error/GraphQLError.swift +++ b/Sources/GraphQL/Error/GraphQLError.swift @@ -169,7 +169,7 @@ extension GraphQLError: Hashable { // MARK: IndexPath -public struct IndexPath: Codable { +public struct IndexPath: Codable, Sendable { public let elements: [IndexPathValue] public init(_ elements: [IndexPathElement] = []) { @@ -197,7 +197,7 @@ extension IndexPath: ExpressibleByArrayLiteral { } } -public enum IndexPathValue: Codable, Equatable { +public enum IndexPathValue: Codable, Equatable, Sendable { case index(Int) case key(String) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index f082b721..4f3dfda2 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -1,5 +1,4 @@ import Dispatch -import NIO import OrderedCollections /** @@ -28,75 +27,69 @@ import OrderedCollections * Namely, schema of the type system that is currently executing, * and the fragments defined in the query document */ -public final class ExecutionContext { - let queryStrategy: QueryFieldExecutionStrategy - let mutationStrategy: MutationFieldExecutionStrategy - let subscriptionStrategy: SubscriptionFieldExecutionStrategy - let instrumentation: Instrumentation +public final class ExecutionContext: @unchecked Sendable { + let queryStrategy: QueryFieldExecutionStrategy = ConcurrentFieldExecutionStrategy() + let mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy() + let subscriptionStrategy: SubscriptionFieldExecutionStrategy = + ConcurrentFieldExecutionStrategy() public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] - public let rootValue: Any - public let context: Any - public let eventLoopGroup: EventLoopGroup + public let rootValue: any Sendable + public let context: any Sendable public let operation: OperationDefinition public let variableValues: [String: Map] - private var errorsSemaphore = DispatchSemaphore(value: 1) private var _errors: [GraphQLError] - + private let errorsQueue = DispatchQueue( + label: "graphql.schema.validationerrors", + attributes: .concurrent + ) public var errors: [GraphQLError] { - errorsSemaphore.wait() - defer { - errorsSemaphore.signal() + get { + // Reads can occur concurrently. + return errorsQueue.sync { + _errors + } + } + set { + // Writes occur sequentially. + return errorsQueue.async(flags: .barrier) { + self._errors = newValue + } } - return _errors } init( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, fragments: [String: FragmentDefinition], - rootValue: Any, - context: Any, - eventLoopGroup: EventLoopGroup, + rootValue: any Sendable, + context: any Sendable, operation: OperationDefinition, variableValues: [String: Map], errors: [GraphQLError] ) { - self.queryStrategy = queryStrategy - self.mutationStrategy = mutationStrategy - self.subscriptionStrategy = subscriptionStrategy - self.instrumentation = instrumentation self.schema = schema self.fragments = fragments self.rootValue = rootValue self.context = context - self.eventLoopGroup = eventLoopGroup self.operation = operation self.variableValues = variableValues _errors = errors } public func append(error: GraphQLError) { - errorsSemaphore.wait() - defer { - errorsSemaphore.signal() - } - _errors.append(error) + errors.append(error) } } -public protocol FieldExecutionStrategy { +public protocol FieldExecutionStrategy: Sendable { func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - sourceValue: Any, + sourceValue: any Sendable, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> + ) async throws -> OrderedDictionary } public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {} @@ -114,33 +107,23 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - sourceValue: Any, + sourceValue: any Sendable, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> { - var results = OrderedDictionary() - - return fields - .reduce(exeContext.eventLoopGroup.next().makeSucceededVoidFuture()) { prev, field in - // We use ``flatSubmit`` here to avoid a stack overflow issue with EventLoopFutures. - // See: https://github.com/apple/swift-nio/issues/970 - exeContext.eventLoopGroup.next().flatSubmit { - prev.tryFlatMap { - let fieldASTs = field.value - let fieldPath = path.appending(field.key) - - return try resolveField( - exeContext: exeContext, - parentType: parentType, - source: sourceValue, - fieldASTs: fieldASTs, - path: fieldPath - ).map { result in - results[field.key] = result ?? Map.null - } - } - } - }.map { results } + ) async throws -> OrderedDictionary { + var results = OrderedDictionary() + for field in fields { + let fieldASTs = field.value + let fieldPath = path.appending(field.key) + results[field.key] = try await resolveField( + exeContext: exeContext, + parentType: parentType, + source: sourceValue, + fieldASTs: fieldASTs, + path: fieldPath + ) ?? Map.null + } + return results } } @@ -149,78 +132,39 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, * * Each field is resolved as an individual task on a concurrent dispatch queue. */ -public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrategy, +public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, SubscriptionFieldExecutionStrategy { - let dispatchQueue: DispatchQueue - - public init(dispatchQueue: DispatchQueue) { - self.dispatchQueue = dispatchQueue - } - - public init( - queueLabel: String = "GraphQL field execution", - queueQoS: DispatchQoS = .userInitiated - ) { - dispatchQueue = DispatchQueue( - label: queueLabel, - qos: queueQoS, - attributes: .concurrent - ) - } - public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - sourceValue: Any, + sourceValue: any Sendable, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> { - let resultsQueue = DispatchQueue( - label: "\(dispatchQueue.label) results", - qos: dispatchQueue.qos - ) - - let group = DispatchGroup() - // preserve field order by assigning to null and filtering later - var results: OrderedDictionary?> = fields - .mapValues { _ -> Future? in nil } - var err: Error? - - for field in fields { - let fieldASTs = field.value - let fieldKey = field.key - let fieldPath = path.appending(fieldKey) - dispatchQueue.async(group: group) { - guard err == nil else { - return - } - do { - let result = try resolveField( + ) async throws -> OrderedDictionary { + return try await withThrowingTaskGroup(of: (String, (any Sendable)?).self) { group in + // preserve field order by assigning to null and filtering later + var results: OrderedDictionary = fields + .mapValues { _ -> Any? in nil } + for field in fields { + group.addTask { + let fieldASTs = field.value + let fieldPath = path.appending(field.key) + let result = try await resolveField( exeContext: exeContext, parentType: parentType, source: sourceValue, fieldASTs: fieldASTs, path: fieldPath - ) - resultsQueue.async(group: group) { - results[fieldKey] = result.map { $0 ?? Map.null } - } - } catch { - resultsQueue.async(group: group) { - err = error - } + ) ?? Map.null + return (field.key, result) } } + for try await result in group { + results[result.0] = result.1 + } + return results.compactMapValues { $0 } } - - group.wait() - - if let error = err { - throw error - } - - return results.compactMapValues { $0 }.flatten(on: exeContext.eventLoopGroup) } } @@ -230,113 +174,58 @@ public struct ConcurrentDispatchFieldExecutionStrategy: QueryFieldExecutionStrat * If the arguments to this func do not result in a legal execution context, * a GraphQLError will be thrown immediately explaining the invalid input. */ -func execute( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, +public func execute( schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, - eventLoopGroup: EventLoopGroup, + rootValue: any Sendable, + context: any Sendable, variableValues: [String: Map] = [:], operationName: String? = nil -) -> Future { - let executeStarted = instrumentation.now +) async throws -> GraphQLResult { let buildContext: ExecutionContext do { // If a valid context cannot be created due to incorrect arguments, // this will throw an error. buildContext = try buildExecutionContext( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, rawVariableValues: variableValues, operationName: operationName ) } catch let error as GraphQLError { - instrumentation.operationExecution( - processId: processId(), - threadId: threadId(), - started: executeStarted, - finished: instrumentation.now, - schema: schema, - document: documentAST, - rootValue: rootValue, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operation: nil, - errors: [error], - result: nil - ) - - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error])) + return GraphQLResult(errors: [error]) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)])) + return GraphQLResult(errors: [GraphQLError(error)]) } do { // var executeErrors: [GraphQLError] = [] - - return try executeOperation( + let data = try await executeOperation( exeContext: buildContext, operation: buildContext.operation, rootValue: rootValue - ).flatMapThrowing { data -> GraphQLResult in - var dataMap: Map = [:] + ) + var dataMap: Map = [:] - for (key, value) in data { - dataMap[key] = try map(from: value) - } + for (key, value) in data { + dataMap[key] = try map(from: value) + } - var result: GraphQLResult = .init(data: dataMap) + var result: GraphQLResult = .init(data: dataMap) - if !buildContext.errors.isEmpty { - result.errors = buildContext.errors - } + if !buildContext.errors.isEmpty { + result.errors = buildContext.errors + } // executeErrors = buildContext.errors - return result - }.flatMapError { error -> Future in - let result: GraphQLResult - if let error = error as? GraphQLError { - result = GraphQLResult(errors: [error]) - } else { - result = GraphQLResult(errors: [GraphQLError(error)]) - } - - return buildContext.eventLoopGroup.next().makeSucceededFuture(result) - }.map { result -> GraphQLResult in -// instrumentation.operationExecution( -// processId: processId(), -// threadId: threadId(), -// started: executeStarted, -// finished: instrumentation.now, -// schema: schema, -// document: documentAST, -// rootValue: rootValue, -// eventLoopGroup: eventLoopGroup, -// variableValues: variableValues, -// operation: buildContext.operation, -// errors: executeErrors, -// result: result -// ) - result - } + return result } catch let error as GraphQLError { - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: [error])) + return GraphQLResult(errors: [error]) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(GraphQLResult(errors: [GraphQLError(error)])) + return GraphQLResult(errors: [GraphQLError(error)]) } } @@ -347,15 +236,10 @@ func execute( * Throws a GraphQLError if a valid execution context cannot be created. */ func buildExecutionContext( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, - eventLoopGroup: EventLoopGroup, + rootValue: any Sendable, + context: any Sendable, rawVariableValues: [String: Map], operationName: String? ) throws -> ExecutionContext { @@ -402,15 +286,10 @@ func buildExecutionContext( ) return ExecutionContext( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, fragments: fragments, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, operation: operation, variableValues: variableValues, errors: errors @@ -423,8 +302,8 @@ func buildExecutionContext( func executeOperation( exeContext: ExecutionContext, operation: OperationDefinition, - rootValue: Any -) throws -> Future> { + rootValue: any Sendable +) async throws -> OrderedDictionary { let type = try getOperationRootType(schema: exeContext.schema, operation: operation) var inputFields: OrderedDictionary = [:] var visitedFragmentNames: [String: Bool] = [:] @@ -448,7 +327,7 @@ func executeOperation( fieldExecutionStrategy = exeContext.subscriptionStrategy } - return try fieldExecutionStrategy.executeFields( + return try await fieldExecutionStrategy.executeFields( exeContext: exeContext, parentType: type, sourceValue: rootValue, @@ -684,10 +563,10 @@ func getFieldEntryKey(node: Field) -> String { public func resolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, - source: Any, + source: any Sendable, fieldASTs: [Field], path: IndexPath -) throws -> Future { +) async throws -> (any Sendable)? { let fieldAST = fieldASTs[0] let fieldName = fieldAST.name.value @@ -729,32 +608,17 @@ public func resolveField( variableValues: exeContext.variableValues ) -// let resolveFieldStarted = exeContext.instrumentation.now - // Get the resolve func, regardless of if its result is normal // or abrupt (error). - let result = resolveOrError( + let result = await resolveOrError( resolve: resolve, source: source, args: args, context: context, - eventLoopGroup: exeContext.eventLoopGroup, info: info ) -// exeContext.instrumentation.fieldResolution( -// processId: processId(), -// threadId: threadId(), -// started: resolveFieldStarted, -// finished: exeContext.instrumentation.now, -// source: source, -// args: args, -// eventLoopGroup: exeContext.eventLoopGroup, -// info: info, -// result: result -// ) - - return try completeValueCatchingError( + return try await completeValueCatchingError( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, @@ -768,14 +632,13 @@ public func resolveField( // function. Returns the result of `resolve` or the abrupt-return Error object. func resolveOrError( resolve: GraphQLFieldResolve, - source: Any, + source: any Sendable, args: Map, - context: Any, - eventLoopGroup: EventLoopGroup, + context: any Sendable, info: GraphQLResolveInfo -) -> Result, Error> { +) async -> Result<(any Sendable)?, Error> { do { - let result = try resolve(source, args, context, eventLoopGroup, info) + let result = try await resolve(source, args, context, info) return .success(result) } catch { return .failure(error) @@ -790,12 +653,12 @@ func completeValueCatchingError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result<(any Sendable)?, Error> +) async throws -> (any Sendable)? { // If the field type is non-nullable, then it is resolved without any // protection from errors, however it still properly locates the error. if let returnType = returnType as? GraphQLNonNull { - return try completeValueWithLocatedError( + return try await completeValueWithLocatedError( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, @@ -808,29 +671,21 @@ func completeValueCatchingError( // Otherwise, error protection is applied, logging the error and resolving // a null value for this field if one is encountered. do { - let completed = try completeValueWithLocatedError( + return try await completeValueWithLocatedError( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, info: info, path: path, result: result - ).flatMapError { error -> EventLoopFuture in - guard let error = error as? GraphQLError else { - return exeContext.eventLoopGroup.next().makeFailedFuture(error) - } - exeContext.append(error: error) - return exeContext.eventLoopGroup.next().makeSucceededFuture(nil) - } - - return completed + ) } catch let error as GraphQLError { // If `completeValueWithLocatedError` returned abruptly (threw an error), // log the error and return .null. exeContext.append(error: error) - return exeContext.eventLoopGroup.next().makeSucceededFuture(nil) + return nil } catch { - return exeContext.eventLoopGroup.next().makeFailedFuture(error) + throw error } } @@ -842,20 +697,17 @@ func completeValueWithLocatedError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result<(any Sendable)?, Error> +) async throws -> (any Sendable)? { do { - let completed = try completeValue( + return try await completeValue( exeContext: exeContext, returnType: returnType, fieldASTs: fieldASTs, info: info, path: path, result: result - ).flatMapErrorThrowing { error -> Any? in - throw locatedError(originalError: error, nodes: fieldASTs, path: path) - } - return completed + ) } catch { throw locatedError( originalError: error, @@ -892,8 +744,8 @@ func completeValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result<(any Sendable)?, Error> +) async throws -> (any Sendable)? { switch result { case let .failure(error): throw error @@ -901,79 +753,75 @@ func completeValue( // If field type is NonNull, complete for inner type, and throw field error // if result is nullish. if let returnType = returnType as? GraphQLNonNull { - return try completeValue( + let value = try await completeValue( exeContext: exeContext, returnType: returnType.ofType, fieldASTs: fieldASTs, info: info, path: path, result: .success(result) - ).flatMapThrowing { value -> Any? in - guard let value = value else { - throw GraphQLError( - message: "Cannot return null for non-nullable field \(info.parentType.name).\(info.fieldName)." - ) - } - - return value + ) + guard let value = value else { + throw GraphQLError( + message: "Cannot return null for non-nullable field \(info.parentType.name).\(info.fieldName)." + ) } - } - return result.tryFlatMap { result throws -> Future in - // If result value is null-ish (nil or .null) then return .null. - guard let result = result, let r = unwrap(result) else { - return exeContext.eventLoopGroup.next().makeSucceededFuture(nil) - } + return value + } - // If field type is List, complete each item in the list with the inner type - if let returnType = returnType as? GraphQLList { - return try completeListValue( - exeContext: exeContext, - returnType: returnType, - fieldASTs: fieldASTs, - info: info, - path: path, - result: r - ).map { $0 } - } + // If result value is null-ish (nil or .null) then return .null. + guard let result = result, let r = unwrap(result) else { + return nil + } - // If field type is a leaf type, Scalar or Enum, serialize to a valid value, - // returning .null if serialization is not possible. - if let returnType = returnType as? GraphQLLeafType { - return try exeContext.eventLoopGroup.next() - .makeSucceededFuture(completeLeafValue(returnType: returnType, result: r)) - } + // If field type is List, complete each item in the list with the inner type + if let returnType = returnType as? GraphQLList { + return try await completeListValue( + exeContext: exeContext, + returnType: returnType, + fieldASTs: fieldASTs, + info: info, + path: path, + result: r + ) + } - // If field type is an abstract type, Interface or Union, determine the - // runtime Object type and complete for that type. - if let returnType = returnType as? GraphQLAbstractType { - return try completeAbstractValue( - exeContext: exeContext, - returnType: returnType, - fieldASTs: fieldASTs, - info: info, - path: path, - result: r - ) - } + // If field type is a leaf type, Scalar or Enum, serialize to a valid value, + // returning .null if serialization is not possible. + if let returnType = returnType as? GraphQLLeafType { + return try completeLeafValue(returnType: returnType, result: r) + } - // If field type is Object, execute and complete all sub-selections. - if let returnType = returnType as? GraphQLObjectType { - return try completeObjectValue( - exeContext: exeContext, - returnType: returnType, - fieldASTs: fieldASTs, - info: info, - path: path, - result: r - ) - } + // If field type is an abstract type, Interface or Union, determine the + // runtime Object type and complete for that type. + if let returnType = returnType as? GraphQLAbstractType { + return try await completeAbstractValue( + exeContext: exeContext, + returnType: returnType, + fieldASTs: fieldASTs, + info: info, + path: path, + result: r + ) + } - // Not reachable. All possible output types have been considered. - throw GraphQLError( - message: "Cannot complete value of unexpected type \"\(returnType)\"." + // If field type is Object, execute and complete all sub-selections. + if let returnType = returnType as? GraphQLObjectType { + return try await completeObjectValue( + exeContext: exeContext, + returnType: returnType, + fieldASTs: fieldASTs, + info: info, + path: path, + result: r ) } + + // Not reachable. All possible output types have been considered. + throw GraphQLError( + message: "Cannot complete value of unexpected type \"\(returnType)\"." + ) } } @@ -987,9 +835,9 @@ func completeListValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Any -) throws -> Future<[Any?]> { - guard let result = result as? [Any?] else { + result: any Sendable +) async throws -> [(any Sendable)?] { + guard let result = result as? [(any Sendable)?] else { throw GraphQLError( message: "Expected array, but did not find one for field " + @@ -998,35 +846,39 @@ func completeListValue( } let itemType = returnType.ofType - var completedResults: [Future] = [] - - for (index, item) in result.enumerated() { - // No need to modify the info object containing the path, - // since from here on it is not ever accessed by resolver funcs. - let fieldPath = path.appending(index) - let futureItem = item as? Future ?? exeContext.eventLoopGroup.next() - .makeSucceededFuture(item) - let completedItem = try completeValueCatchingError( - exeContext: exeContext, - returnType: itemType, - fieldASTs: fieldASTs, - info: info, - path: fieldPath, - result: .success(futureItem) - ) + return try await withThrowingTaskGroup(of: (Int, (any Sendable)?).self) { group in + // To preserve order, match size to result, and filter out nils at the end. + var results: [(any Sendable)?] = result.map { _ in nil } + for (index, item) in result.enumerated() { + group.addTask { + // No need to modify the info object containing the path, + // since from here on it is not ever accessed by resolver funcs. + let fieldPath = path.appending(index) - completedResults.append(completedItem) + let result = try await completeValueCatchingError( + exeContext: exeContext, + returnType: itemType, + fieldASTs: fieldASTs, + info: info, + path: fieldPath, + result: .success(item) + ) + return (index, result) + } + for try await result in group { + results[result.0] = result.1 + } + } + return results.compactMap { $0 } } - - return completedResults.flatten(on: exeContext.eventLoopGroup) } /** * Complete a Scalar or Enum by serializing to a valid value, returning * .null if serialization is not possible. */ -func completeLeafValue(returnType: GraphQLLeafType, result: Any?) throws -> Map { +func completeLeafValue(returnType: GraphQLLeafType, result: (any Sendable)?) throws -> Map { guard let result = result else { return .null } @@ -1047,14 +899,13 @@ func completeAbstractValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Any -) throws -> Future { - var resolveRes = try returnType.resolveType?(result, exeContext.eventLoopGroup, info) + result: any Sendable +) async throws -> (any Sendable)? { + var resolveRes = try returnType.resolveType?(result, info) .typeResolveResult resolveRes = try resolveRes ?? defaultResolveType( value: result, - eventLoopGroup: exeContext.eventLoopGroup, info: info, abstractType: returnType ) @@ -1095,7 +946,7 @@ func completeAbstractValue( ) } - return try completeObjectValue( + return try await completeObjectValue( exeContext: exeContext, returnType: objectType, fieldASTs: fieldASTs, @@ -1114,14 +965,14 @@ func completeObjectValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Any -) throws -> Future { + result: any Sendable +) async throws -> (any Sendable)? { // If there is an isTypeOf predicate func, call it with the // current result. If isTypeOf returns false, then raise an error rather // than continuing execution. if let isTypeOf = returnType.isTypeOf, - try !isTypeOf(result, exeContext.eventLoopGroup, info) + try !isTypeOf(result, info) { throw GraphQLError( message: @@ -1146,13 +997,13 @@ func completeObjectValue( } } - return try exeContext.queryStrategy.executeFields( + return try await exeContext.queryStrategy.executeFields( exeContext: exeContext, parentType: returnType, sourceValue: result, path: path, fields: subFieldASTs - ).map { $0 } + ) } /** @@ -1161,8 +1012,7 @@ func completeObjectValue( * isTypeOf for the object being coerced, returning the first type that matches. */ func defaultResolveType( - value: Any, - eventLoopGroup: EventLoopGroup, + value: any Sendable, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType ) throws -> TypeResolveResult? { @@ -1170,7 +1020,7 @@ func defaultResolveType( guard let type = try possibleTypes - .find({ try $0.isTypeOf?(value, eventLoopGroup, info) ?? false }) + .find({ try $0.isTypeOf?(value, info) ?? false }) else { return nil } @@ -1184,34 +1034,33 @@ func defaultResolveType( * and returns it as the result. */ func defaultResolve( - source: Any, + source: any Sendable, args _: Map, - context _: Any, - eventLoopGroup: EventLoopGroup, + context _: any Sendable, info: GraphQLResolveInfo -) -> Future { +) async throws -> (any Sendable)? { guard let source = unwrap(source) else { - return eventLoopGroup.next().makeSucceededFuture(nil) + return nil } if let subscriptable = source as? KeySubscriptable { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } - if let subscriptable = source as? [String: Any] { + if let subscriptable = source as? [String: any Sendable] { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } - if let subscriptable = source as? OrderedDictionary { + if let subscriptable = source as? OrderedDictionary { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } let mirror = Mirror(reflecting: source) guard let value = mirror.getValue(named: info.fieldName) else { - return eventLoopGroup.next().makeSucceededFuture(nil) + return nil } - return eventLoopGroup.next().makeSucceededFuture(value) + return value } /** diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 0cc6c643..cd66ba88 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -1,4 +1,3 @@ -import NIO public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertible { public var data: Map? @@ -43,21 +42,15 @@ public struct GraphQLResult: Equatable, Codable, Sendable, CustomStringConvertib } } -/// SubscriptionResult wraps the observable and error data returned by the subscribe request. -public struct SubscriptionResult { - public let stream: SubscriptionEventStream? +/// A collection of GraphQL errors. Enables returning multiple errors from Result types. +public struct GraphQLErrors: Error, Sendable { public let errors: [GraphQLError] - public init(stream: SubscriptionEventStream? = nil, errors: [GraphQLError] = []) { - self.stream = stream + public init(_ errors: [GraphQLError]) { self.errors = errors } } -/// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription -/// results. Subscribers can be added to this stream. -public typealias SubscriptionEventStream = EventStream> - /// This is the primary entry point function for fulfilling GraphQL operations /// by parsing, validating, and executing a GraphQL document along side a /// GraphQL schema. @@ -66,11 +59,6 @@ public typealias SubscriptionEventStream = EventStream> /// 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 @@ -92,42 +80,40 @@ public typealias SubscriptionEventStream = EventStream> /// 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, - validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, + rootValue: (any Sendable) = (), + context: (any Sendable) = (), variableValues: [String: Map] = [:], - operationName: String? = nil -) throws -> Future { + operationName: String? = nil, + validationRules: [@Sendable (ValidationContext) -> Visitor] = specifiedRules +) async throws -> GraphQLResult { + // Validate schema + let schemaValidationErrors = try validateSchema(schema: schema) + guard schemaValidationErrors.isEmpty else { + return GraphQLResult(errors: schemaValidationErrors) + } + + // Parse let source = Source(body: request, name: "GraphQL request") - let documentAST = try parse(instrumentation: instrumentation, source: source) + let documentAST = try parse(source: source) + + // Validate let validationErrors = validate( - instrumentation: instrumentation, schema: schema, ast: documentAST, rules: validationRules ) - guard validationErrors.isEmpty else { - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: validationErrors)) + return GraphQLResult(errors: validationErrors) } - return execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, + // Execute + return try await execute( schema: schema, documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) @@ -136,11 +122,6 @@ public func graphql( /// This is the primary entry point function for fulfilling GraphQL operations /// by using persisted queries. /// -/// - 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 queryRetrieval: The PersistedQueryRetrieval instance to use for looking up /// queries /// - parameter queryId: The id of the query to execute @@ -161,36 +142,26 @@ public func graphql( /// 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, queryRetrieval: Retrieval, queryId: Retrieval.Id, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, + rootValue: (any Sendable) = (), + context: (any Sendable) = (), variableValues: [String: Map] = [:], operationName: String? = nil -) throws -> Future { +) async throws -> GraphQLResult { switch try queryRetrieval.lookup(queryId) { case .unknownId: throw GraphQLError(message: "Unknown query id") case let .parseError(parseError): throw parseError case let .validateErrors(_, validationErrors): - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: validationErrors)) + return GraphQLResult(errors: validationErrors) case let .result(schema, documentAST): - return execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, + return try await execute( schema: schema, documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) @@ -205,11 +176,6 @@ public func graphql( /// 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 @@ -235,177 +201,32 @@ public func graphql( /// 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, - validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, - rootValue: Any = (), - context: Any = (), - eventLoopGroup: EventLoopGroup, + rootValue: (any Sendable) = (), + context: (any Sendable) = (), variableValues: [String: Map] = [:], - operationName: String? = nil -) throws -> Future { + operationName: String? = nil, + validationRules: [@Sendable (ValidationContext) -> Visitor] = specifiedRules +) async throws -> Result, GraphQLErrors> { let source = Source(body: request, name: "GraphQL Subscription request") - let documentAST = try parse(instrumentation: instrumentation, source: source) + let documentAST = try parse(source: source) let validationErrors = validate( - instrumentation: instrumentation, schema: schema, ast: documentAST, rules: validationRules ) guard validationErrors.isEmpty else { - return eventLoopGroup.next() - .makeSucceededFuture(SubscriptionResult(errors: validationErrors)) + return .failure(.init(validationErrors)) } - return subscribe( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, + return try await subscribe( schema: schema, documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) } - -// MARK: Async/Await - -/// 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. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -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() -} - -/// 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. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -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/GraphQLRequest.swift b/Sources/GraphQL/GraphQLRequest.swift index bfcca8ff..035ae174 100644 --- a/Sources/GraphQL/GraphQLRequest.swift +++ b/Sources/GraphQL/GraphQLRequest.swift @@ -37,7 +37,6 @@ public struct GraphQLRequest: Equatable, Codable { /// - Returns: The operation type performed by the request public func operationType() throws -> OperationType { let documentAST = try GraphQL.parse( - instrumentation: NoOpInstrumentation, source: Source(body: query, name: "GraphQL request") ) let firstOperation = documentAST.definitions.compactMap { $0 as? OperationDefinition }.first diff --git a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift b/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift deleted file mode 100644 index 73d54278..00000000 --- a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift +++ /dev/null @@ -1,141 +0,0 @@ -import Dispatch -import NIO - -/// Proxies calls through to another `Instrumentation` instance via a DispatchQueue -/// -/// Has two primary use cases: -/// 1. Allows a non thread safe Instrumentation implementation to be used along side a multithreaded -/// execution strategy -/// 2. Allows slow or heavy instrumentation processing to happen outside of the current query -/// execution -public class DispatchQueueInstrumentationWrapper: Instrumentation { - let instrumentation: Instrumentation - let dispatchQueue: DispatchQueue - let dispatchGroup: DispatchGroup? - - public init( - _ instrumentation: Instrumentation, - label: String = "GraphQL instrumentation wrapper", - qos: DispatchQoS = .utility, - attributes: DispatchQueue.Attributes = [], - dispatchGroup: DispatchGroup? = nil - ) { - self.instrumentation = instrumentation - dispatchQueue = DispatchQueue(label: label, qos: qos, attributes: attributes) - self.dispatchGroup = dispatchGroup - } - - public init( - _ instrumentation: Instrumentation, - dispatchQueue: DispatchQueue, - dispatchGroup: DispatchGroup? = nil - ) { - self.instrumentation = instrumentation - self.dispatchQueue = dispatchQueue - self.dispatchGroup = dispatchGroup - } - - public var now: DispatchTime { - return instrumentation.now - } - - public func queryParsing( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Source, - result: Result - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.queryParsing( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - source: source, - result: result - ) - } - } - - public func queryValidation( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - errors: [GraphQLError] - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.queryValidation( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - schema: schema, - document: document, - errors: errors - ) - } - } - - public func operationExecution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - rootValue: Any, - eventLoopGroup: EventLoopGroup, - variableValues: [String: Map], - operation: OperationDefinition?, - errors: [GraphQLError], - result: Map - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.operationExecution( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - schema: schema, - document: document, - rootValue: rootValue, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operation: operation, - errors: errors, - result: result - ) - } - } - - public func fieldResolution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Any, - args: Map, - eventLoopGroup: EventLoopGroup, - info: GraphQLResolveInfo, - result: Result, Error> - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.fieldResolution( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - source: source, - args: args, - eventLoopGroup: eventLoopGroup, - info: info, - result: result - ) - } - } -} diff --git a/Sources/GraphQL/Instrumentation/Instrumentation.swift b/Sources/GraphQL/Instrumentation/Instrumentation.swift deleted file mode 100644 index 1a315ab0..00000000 --- a/Sources/GraphQL/Instrumentation/Instrumentation.swift +++ /dev/null @@ -1,126 +0,0 @@ -import Dispatch -import Foundation -import NIO - -/// Provides the capability to instrument the execution steps of a GraphQL query. -/// -/// A working implementation of `now` is also provided by default. -public protocol Instrumentation { - var now: DispatchTime { get } - - func queryParsing( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Source, - result: Result - ) - - func queryValidation( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - errors: [GraphQLError] - ) - - func operationExecution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - schema: GraphQLSchema, - document: Document, - rootValue: Any, - eventLoopGroup: EventLoopGroup, - variableValues: [String: Map], - operation: OperationDefinition?, - errors: [GraphQLError], - result: Map - ) - - func fieldResolution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Any, - args: Map, - eventLoopGroup: EventLoopGroup, - info: GraphQLResolveInfo, - result: Result, Error> - ) -} - -public extension Instrumentation { - var now: DispatchTime { - return DispatchTime.now() - } -} - -func threadId() -> Int { - #if os(Linux) || os(Android) - return Int(pthread_self()) - #else - return Int(pthread_mach_thread_np(pthread_self())) - #endif -} - -func processId() -> Int { - return Int(getpid()) -} - -/// Does nothing -public let NoOpInstrumentation: Instrumentation = noOpInstrumentation() - -struct noOpInstrumentation: Instrumentation { - public let now = DispatchTime(uptimeNanoseconds: 0) - public func queryParsing( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Source, - result _: Result - ) {} - - public func queryValidation( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - errors _: [GraphQLError] - ) {} - - public func operationExecution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - rootValue _: Any, - eventLoopGroup _: EventLoopGroup, - variableValues _: [String: Map], - operation _: OperationDefinition?, - errors _: [GraphQLError], - result _: Map - ) {} - - public func fieldResolution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Any, - args _: Map, - eventLoopGroup _: EventLoopGroup, - info _: GraphQLResolveInfo, - result _: Result, Error> - ) {} -} diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 97274af3..72ee37d8 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -2,7 +2,7 @@ * Contains a range of UTF-8 character offsets and token references that * identify the region of the source from which the AST derived. */ -public struct Location: Equatable { +public struct Location: Equatable, Sendable { /** * The character offset at which this Node begins. */ @@ -33,8 +33,10 @@ public struct Location: Equatable { * Represents a range of characters represented by a lexical token * within a Source. */ -public final class Token { - public enum Kind: String, CustomStringConvertible { +public final class Token: @unchecked Sendable { + /// Token is sendable because it is not mutated after being Lexed, which is controlled + /// internally. + public enum Kind: String, CustomStringConvertible, Sendable { case sof = "" case eof = "" case bang = "!" @@ -93,7 +95,7 @@ public final class Token { * including ignored tokens. is always the first node and * the last. */ - public internal(set) weak var prev: Token? + public let prev: Token? public internal(set) var next: Token? init( @@ -181,8 +183,7 @@ public enum NodeResult { guard let key = key.keyValue else { return nil } - node.set(value: value, key: key) - return .node(node) + return .node(node.set(value: value, key: key)) case var .array(array): switch value { case let .node(value): @@ -201,11 +202,11 @@ public enum NodeResult { /** * The list of all possible AST node types. */ -public protocol Node { +public protocol Node: Sendable { var kind: Kind { get } var loc: Location? { get } func get(key: String) -> NodeResult? - func set(value: NodeResult?, key: String) + func set(value: NodeResult?, key: String) -> Self } public extension Node { @@ -213,13 +214,9 @@ public extension Node { return nil } - @available(*, deprecated, message: "Use set(value _: NodeResult?, key _: String)") - func set(value: Node?, key: String) { - return set(value: value.map { .node($0) }, key: key) - } - - func set(value _: NodeResult?, key _: String) { + func set(value _: NodeResult?, key _: String) -> Self { // This should be overridden by each type on which it should do something + return self } } @@ -261,7 +258,7 @@ extension TypeExtensionDefinition: Node {} extension SchemaExtensionDefinition: Node {} extension DirectiveDefinition: Node {} -public final class Name { +public struct Name: Sendable { public let kind: Kind = .name public let loc: Location? public let value: String @@ -278,7 +275,7 @@ extension Name: Equatable { } } -public final class Document { +public struct Document { public let kind: Kind = .document public let loc: Location? public private(set) var definitions: [Definition] @@ -300,22 +297,24 @@ public final class Document { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "definitions": guard case let .array(values) = value, let definitions = values as? [Definition] else { - return + break } - self.definitions = definitions + new.definitions = definitions default: - return + break } + return new } } @@ -360,7 +359,7 @@ public func == (lhs: Definition, rhs: Definition) -> Bool { return false } -public enum OperationType: String, CaseIterable { +public enum OperationType: String, CaseIterable, Sendable { case query case mutation case subscription @@ -370,10 +369,10 @@ public final class OperationDefinition { public let kind: Kind = .operationDefinition public let loc: Location? public let operation: OperationType - public private(set) var name: Name? - public private(set) var variableDefinitions: [VariableDefinition] - public private(set) var directives: [Directive] - public private(set) var selectionSet: SelectionSet + public let name: Name? + public let variableDefinitions: [VariableDefinition] + public let directives: [Directive] + public let selectionSet: SelectionSet init( loc: Location? = nil, @@ -412,9 +411,9 @@ public final class OperationDefinition { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } switch key { case "name": @@ -422,35 +421,63 @@ public final class OperationDefinition { case let .node(node) = value, let name = node as? Name else { - return + return self } - self.name = name + return Self( + loc: loc, + operation: operation, + name: name, + variableDefinitions: variableDefinitions, + directives: directives, + selectionSet: selectionSet + ) case "variableDefinitions": guard case let .array(values) = value, let variableDefinitions = values as? [VariableDefinition] else { - return + return self } - self.variableDefinitions = variableDefinitions + return Self( + loc: loc, + operation: operation, + name: name, + variableDefinitions: variableDefinitions, + directives: directives, + selectionSet: selectionSet + ) case "directives": guard case let .array(values) = value, let directives = values as? [Directive] else { - return + return self } - self.directives = directives + return Self( + loc: loc, + operation: operation, + name: name, + variableDefinitions: variableDefinitions, + directives: directives, + selectionSet: selectionSet + ) case "selectionSet": guard case let .node(value) = value, let selectionSet = value as? SelectionSet else { - return + return self } - self.selectionSet = selectionSet + return Self( + loc: loc, + operation: operation, + name: name, + variableDefinitions: variableDefinitions, + directives: directives, + selectionSet: selectionSet + ) default: - return + return self } } } @@ -469,7 +496,7 @@ extension OperationDefinition: Hashable { } } -public final class VariableDefinition { +public struct VariableDefinition { public let kind: Kind = .variableDefinition public let loc: Location? public private(set) var variable: Variable @@ -509,46 +536,48 @@ public final class VariableDefinition { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "variable": guard case let .node(node) = value, let variable = node as? Variable else { - return + break } - self.variable = variable + new.variable = variable case "type": guard case let .node(node) = value, let type = node as? Type else { - return + break } - self.type = type + new.type = type case "defaultValue": guard case let .node(node) = value, let defaultValue = node as? Value? else { - return + break } - self.defaultValue = defaultValue + new.defaultValue = defaultValue case "directives": guard case let .array(values) = value, let directives = values as? [Directive] else { - return + break } - self.directives = directives + new.directives = directives default: - return + break } + return new } } @@ -574,7 +603,7 @@ extension VariableDefinition: Equatable { } } -public final class Variable { +public struct Variable { public let kind: Kind = .variable public let loc: Location? public private(set) var name: Name @@ -593,22 +622,24 @@ public final class Variable { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name default: - return + break } + return new } } @@ -621,7 +652,7 @@ extension Variable: Equatable { public final class SelectionSet { public let kind: Kind = .selectionSet public let loc: Location? - public private(set) var selections: [Selection] + public let selections: [Selection] init(loc: Location? = nil, selections: [Selection]) { self.loc = loc @@ -640,9 +671,9 @@ public final class SelectionSet { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } switch key { case "selections": @@ -650,11 +681,14 @@ public final class SelectionSet { case let .array(values) = value, let selections = values as? [Selection] else { - return + return self } - self.selections = selections + return Self( + loc: loc, + selections: selections + ) default: - return + return self } } } @@ -705,7 +739,7 @@ public func == (lhs: Selection, rhs: Selection) -> Bool { return false } -public final class Field { +public struct Field { public let kind: Kind = .field public let loc: Location? public private(set) var alias: Name? @@ -753,54 +787,56 @@ public final class Field { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "alias": guard case let .node(node) = value, let alias = node as? Name else { - return + break } - self.alias = alias + new.alias = alias case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name case "arguments": guard case let .array(values) = value, let arguments = values as? [Argument] else { - return + break } - self.arguments = arguments + new.arguments = arguments case "directives": guard case let .array(values) = value, let directives = values as? [Directive] else { - return + break } - self.directives = directives + new.directives = directives case "selectionSet": guard case let .node(value) = value, let selectionSet = value as? SelectionSet else { - return + break } - self.selectionSet = selectionSet + new.selectionSet = selectionSet default: - return + break } + return new } } @@ -814,7 +850,7 @@ extension Field: Equatable { } } -public final class Argument { +public struct Argument { public let kind: Kind = .argument public let loc: Location? public private(set) var name: Name @@ -837,30 +873,32 @@ public final class Argument { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name case "value": guard case let .node(node) = value, let value = node as? Value else { - return + break } - self.value = value + new.value = value default: - return + break } + return new } } @@ -875,7 +913,7 @@ public protocol Fragment: Selection {} extension FragmentSpread: Fragment {} extension InlineFragment: Fragment {} -public final class FragmentSpread { +public struct FragmentSpread { public let kind: Kind = .fragmentSpread public let loc: Location? public private(set) var name: Name @@ -901,30 +939,32 @@ public final class FragmentSpread { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name case "directives": guard case let .array(values) = value, let directives = values as? [Directive] else { - return + break } - self.directives = directives + new.directives = directives default: - return + break } + return new } } @@ -951,7 +991,7 @@ extension FragmentDefinition: HasTypeCondition { } } -public final class InlineFragment { +public struct InlineFragment { public let kind: Kind = .inlineFragment public let loc: Location? public private(set) var typeCondition: NamedType? @@ -969,10 +1009,8 @@ public final class InlineFragment { self.directives = directives self.selectionSet = selectionSet } -} -public extension InlineFragment { - func get(key: String) -> NodeResult? { + public func get(key: String) -> NodeResult? { switch key { case "typeCondition": return typeCondition.map { .node($0) } @@ -988,38 +1026,40 @@ public extension InlineFragment { } } - func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "typeCondition": guard case let .node(node) = value, let typeCondition = node as? NamedType else { - return + break } - self.typeCondition = typeCondition + new.typeCondition = typeCondition case "directives": guard case let .array(values) = value, let directives = values as? [Directive] else { - return + break } - self.directives = directives + new.directives = directives case "selectionSet": guard case let .node(value) = value, let selectionSet = value as? SelectionSet else { - return + break } - self.selectionSet = selectionSet + new.selectionSet = selectionSet default: - return + break } + return new } } @@ -1034,10 +1074,10 @@ extension InlineFragment: Equatable { public final class FragmentDefinition { public let kind: Kind = .fragmentDefinition public let loc: Location? - public private(set) var name: Name - public private(set) var typeCondition: NamedType - public private(set) var directives: [Directive] - public private(set) var selectionSet: SelectionSet + public let name: Name + public let typeCondition: NamedType + public let directives: [Directive] + public let selectionSet: SelectionSet init( loc: Location? = nil, @@ -1071,9 +1111,9 @@ public final class FragmentDefinition { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } switch key { case "name": @@ -1081,35 +1121,59 @@ public final class FragmentDefinition { case let .node(node) = value, let name = node as? Name else { - return + return self } - self.name = name + return Self( + loc: loc, + name: name, + typeCondition: typeCondition, + directives: directives, + selectionSet: selectionSet + ) case "typeCondition": guard case let .node(node) = value, let typeCondition = node as? NamedType else { - return + return self } - self.typeCondition = typeCondition + return Self( + loc: loc, + name: name, + typeCondition: typeCondition, + directives: directives, + selectionSet: selectionSet + ) case "directives": guard case let .array(values) = value, let directives = values as? [Directive] else { - return + return self } - self.directives = directives + return Self( + loc: loc, + name: name, + typeCondition: typeCondition, + directives: directives, + selectionSet: selectionSet + ) case "selectionSet": guard case let .node(value) = value, let selectionSet = value as? SelectionSet else { - return + return self } - self.selectionSet = selectionSet + return Self( + loc: loc, + name: name, + typeCondition: typeCondition, + directives: directives, + selectionSet: selectionSet + ) default: - return + return self } } } @@ -1183,7 +1247,7 @@ public func == (lhs: Value, rhs: Value) -> Bool { return false } -public final class IntValue { +public struct IntValue { public let kind: Kind = .intValue public let loc: Location? public let value: String @@ -1200,7 +1264,7 @@ extension IntValue: Equatable { } } -public final class FloatValue { +public struct FloatValue { public let kind: Kind = .floatValue public let loc: Location? public let value: String @@ -1217,7 +1281,7 @@ extension FloatValue: Equatable { } } -public final class StringValue { +public struct StringValue: Sendable { public let kind: Kind = .stringValue public let loc: Location? public let value: String @@ -1236,7 +1300,7 @@ extension StringValue: Equatable { } } -public final class BooleanValue { +public struct BooleanValue { public let kind: Kind = .booleanValue public let loc: Location? public let value: Bool @@ -1253,7 +1317,7 @@ extension BooleanValue: Equatable { } } -public final class NullValue { +public struct NullValue { public let kind: Kind = .nullValue public let loc: Location? @@ -1268,7 +1332,7 @@ extension NullValue: Equatable { } } -public final class EnumValue { +public struct EnumValue { public let kind: Kind = .enumValue public let loc: Location? public let value: String @@ -1285,7 +1349,7 @@ extension EnumValue: Equatable { } } -public final class ListValue { +public struct ListValue { public let kind: Kind = .listValue public let loc: Location? public private(set) var values: [Value] @@ -1304,22 +1368,24 @@ public final class ListValue { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "values": guard case let .array(values) = value, let values = values as? [Value] else { - return + break } - self.values = values + new.values = values default: - return + break } + return new } } @@ -1339,7 +1405,7 @@ extension ListValue: Equatable { } } -public final class ObjectValue { +public struct ObjectValue { public let kind: Kind = .objectValue public let loc: Location? public private(set) var fields: [ObjectField] @@ -1358,22 +1424,24 @@ public final class ObjectValue { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "fields": guard case let .array(values) = value, let fields = values as? [ObjectField] else { - return + break } - self.fields = fields + new.fields = fields default: - return + break } + return new } } @@ -1383,7 +1451,7 @@ extension ObjectValue: Equatable { } } -public final class ObjectField { +public struct ObjectField { public let kind: Kind = .objectField public let loc: Location? public private(set) var name: Name @@ -1406,30 +1474,32 @@ public final class ObjectField { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name case "value": guard case let .node(node) = value, let value = node as? Value else { - return + break } - self.value = value + new.value = value default: - return + break } + return new } } @@ -1440,7 +1510,7 @@ extension ObjectField: Equatable { } } -public final class Directive { +public struct Directive: Sendable { public let kind: Kind = .directive public let loc: Location? public private(set) var name: Name @@ -1463,30 +1533,32 @@ public final class Directive { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name case "arguments": guard case let .array(nodes) = value, let arguments = nodes as? [Argument] else { - return + break } - self.arguments = arguments + new.arguments = arguments default: - return + break } + return new } } @@ -1523,7 +1595,7 @@ public func == (lhs: Type, rhs: Type) -> Bool { return false } -public final class NamedType { +public struct NamedType { public let kind: Kind = .namedType public let loc: Location? public private(set) var name: Name @@ -1542,22 +1614,24 @@ public final class NamedType { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "name": guard case let .node(node) = value, let name = node as? Name else { - return + break } - self.name = name + new.name = name default: - return + break } + return new } } @@ -1567,7 +1641,7 @@ extension NamedType: Equatable { } } -public final class ListType { +public struct ListType { public let kind: Kind = .listType public let loc: Location? public private(set) var type: Type @@ -1586,22 +1660,24 @@ public final class ListType { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "type": guard case let .node(node) = value, let type = node as? Type else { - return + break } - self.type = type + new.type = type default: - return + break } + return new } } @@ -1615,7 +1691,7 @@ public protocol NonNullableType: Type {} extension ListType: NonNullableType {} extension NamedType: NonNullableType {} -public final class NonNullType { +public struct NonNullType { public let kind: Kind = .nonNullType public let loc: Location? public private(set) var type: NonNullableType @@ -1634,22 +1710,24 @@ public final class NonNullType { } } - public func set(value: NodeResult?, key: String) { + public func set(value: NodeResult?, key: String) -> Self { guard let value = value else { - return + return self } + var new = self switch key { case "type": guard case let .node(node) = value, let type = node as? NonNullableType else { - return + break } - self.type = type + new.type = type default: - return + break } + return new } } @@ -1721,7 +1799,7 @@ public func == (lhs: TypeSystemDefinition, rhs: TypeSystemDefinition) -> Bool { return false } -public final class SchemaDefinition { +public struct SchemaDefinition { public let kind: Kind = .schemaDefinition public let loc: Location? public let description: StringValue? @@ -1762,7 +1840,7 @@ extension SchemaDefinition: Equatable { } } -public final class OperationTypeDefinition { +public struct OperationTypeDefinition { public let kind: Kind = .operationDefinition public let loc: Location? public let operation: OperationType @@ -1835,7 +1913,7 @@ public func == (lhs: TypeDefinition, rhs: TypeDefinition) -> Bool { return false } -public final class ScalarTypeDefinition { +public struct ScalarTypeDefinition: Sendable { public let kind: Kind = .scalarTypeDefinition public let loc: Location? public let description: StringValue? @@ -1876,7 +1954,7 @@ extension ScalarTypeDefinition: Equatable { } } -public final class ObjectTypeDefinition { +public struct ObjectTypeDefinition { public let kind: Kind = .objectTypeDefinition public let loc: Location? public let description: StringValue? @@ -1929,7 +2007,7 @@ extension ObjectTypeDefinition: Equatable { } } -public final class FieldDefinition { +public struct FieldDefinition { public let kind: Kind = .fieldDefinition public let loc: Location? public let description: StringValue? @@ -1982,7 +2060,7 @@ extension FieldDefinition: Equatable { } } -public final class InputValueDefinition { +public struct InputValueDefinition: Sendable { public let kind: Kind = .inputValueDefinition public let loc: Location? public let description: StringValue? @@ -2051,7 +2129,7 @@ extension InputValueDefinition: Equatable { } } -public final class InterfaceTypeDefinition { +public struct InterfaceTypeDefinition { public let kind: Kind = .interfaceTypeDefinition public let loc: Location? public let description: StringValue? @@ -2103,7 +2181,7 @@ extension InterfaceTypeDefinition: Equatable { } } -public final class UnionTypeDefinition { +public struct UnionTypeDefinition { public let kind: Kind = .unionTypeDefinition public let loc: Location? public let description: StringValue? @@ -2150,7 +2228,7 @@ extension UnionTypeDefinition: Equatable { } } -public final class EnumTypeDefinition { +public struct EnumTypeDefinition: Sendable { public let kind: Kind = .enumTypeDefinition public let loc: Location? public let description: StringValue? @@ -2197,7 +2275,7 @@ extension EnumTypeDefinition: Equatable { } } -public final class EnumValueDefinition { +public struct EnumValueDefinition: Sendable { public let kind: Kind = .enumValueDefinition public let loc: Location? public let description: StringValue? @@ -2238,7 +2316,7 @@ extension EnumValueDefinition: Equatable { } } -public final class InputObjectTypeDefinition { +public struct InputObjectTypeDefinition { public let kind: Kind = .inputObjectTypeDefinition public let loc: Location? public let description: StringValue? @@ -2296,7 +2374,7 @@ extension UnionExtensionDefinition: TypeExtension {} extension EnumExtensionDefinition: TypeExtension {} extension InputObjectExtensionDefinition: TypeExtension {} -public final class TypeExtensionDefinition { +public struct TypeExtensionDefinition { public let kind: Kind = .typeExtensionDefinition public let loc: Location? public let definition: ObjectTypeDefinition @@ -2321,7 +2399,7 @@ extension TypeExtensionDefinition: Equatable { } } -public final class SchemaExtensionDefinition { +public struct SchemaExtensionDefinition { public let kind: Kind = .schemaExtensionDefinition public let loc: Location? public let definition: SchemaDefinition @@ -2342,7 +2420,7 @@ extension SchemaExtensionDefinition: Equatable { } } -public final class InterfaceExtensionDefinition { +public struct InterfaceExtensionDefinition { public let kind: Kind = .interfaceExtensionDefinition public let loc: Location? public let definition: InterfaceTypeDefinition @@ -2370,7 +2448,7 @@ extension InterfaceExtensionDefinition: Equatable { } } -public final class ScalarExtensionDefinition { +public struct ScalarExtensionDefinition: Sendable { public let kind: Kind = .scalarExtensionDefinition public let loc: Location? public let definition: ScalarTypeDefinition @@ -2397,7 +2475,7 @@ extension ScalarExtensionDefinition: Equatable { } } -public final class UnionExtensionDefinition { +public struct UnionExtensionDefinition { public let kind: Kind = .unionExtensionDefinition public let loc: Location? public let definition: UnionTypeDefinition @@ -2422,7 +2500,7 @@ extension UnionExtensionDefinition: Equatable { } } -public final class EnumExtensionDefinition { +public struct EnumExtensionDefinition: Sendable { public let kind: Kind = .enumExtensionDefinition public let loc: Location? public let definition: EnumTypeDefinition @@ -2447,7 +2525,7 @@ extension EnumExtensionDefinition: Equatable { } } -public final class InputObjectExtensionDefinition { +public struct InputObjectExtensionDefinition { public let kind: Kind = .inputObjectExtensionDefinition public let loc: Location? public let definition: InputObjectTypeDefinition @@ -2475,7 +2553,7 @@ extension InputObjectExtensionDefinition: Equatable { } } -public final class DirectiveDefinition { +public struct DirectiveDefinition { public let kind: Kind = .directiveDefinition public let loc: Location? public let description: StringValue? diff --git a/Sources/GraphQL/Language/Kinds.swift b/Sources/GraphQL/Language/Kinds.swift index 48ba7882..4f638d68 100644 --- a/Sources/GraphQL/Language/Kinds.swift +++ b/Sources/GraphQL/Language/Kinds.swift @@ -1,4 +1,4 @@ -public enum Kind: String, CaseIterable { +public enum Kind: String, CaseIterable, Sendable, Hashable { case name case document case operationDefinition diff --git a/Sources/GraphQL/Language/Location.swift b/Sources/GraphQL/Language/Location.swift index 25f2950c..2c77f1c0 100644 --- a/Sources/GraphQL/Language/Location.swift +++ b/Sources/GraphQL/Language/Location.swift @@ -1,6 +1,6 @@ import Foundation -public struct SourceLocation: Codable, Equatable { +public struct SourceLocation: Codable, Equatable, Sendable { public let line: Int public let column: Int diff --git a/Sources/GraphQL/Language/Parser.swift b/Sources/GraphQL/Language/Parser.swift index 87f9432e..90ef4e29 100644 --- a/Sources/GraphQL/Language/Parser.swift +++ b/Sources/GraphQL/Language/Parser.swift @@ -3,12 +3,10 @@ * Throws GraphQLError if a syntax error is encountered. */ public func parse( - instrumentation: Instrumentation = NoOpInstrumentation, source: String, noLocation: Bool = false ) throws -> Document { return try parse( - instrumentation: instrumentation, source: Source(body: source), noLocation: noLocation ) @@ -19,32 +17,14 @@ public func parse( * Throws GraphQLError if a syntax error is encountered. */ public func parse( - instrumentation: Instrumentation = NoOpInstrumentation, source: Source, noLocation: Bool = false ) throws -> Document { - let started = instrumentation.now do { let lexer = createLexer(source: source, noLocation: noLocation) let document = try parseDocument(lexer: lexer) - instrumentation.queryParsing( - processId: processId(), - threadId: threadId(), - started: started, - finished: instrumentation.now, - source: source, - result: .success(document) - ) return document } catch let error as GraphQLError { - instrumentation.queryParsing( - processId: processId(), - threadId: threadId(), - started: started, - finished: instrumentation.now, - source: source, - result: .failure(error) - ) throw error } } diff --git a/Sources/GraphQL/Language/Source.swift b/Sources/GraphQL/Language/Source.swift index 417c33f9..b934fb5d 100644 --- a/Sources/GraphQL/Language/Source.swift +++ b/Sources/GraphQL/Language/Source.swift @@ -7,7 +7,7 @@ * Note that since Source parsing is heavily UTF8 dependent, the body * is converted into contiguous UTF8 bytes if necessary for optimal performance. */ -public struct Source { +public struct Source: Hashable, Sendable { public let body: String public let name: String diff --git a/Sources/GraphQL/Language/Visitor.swift b/Sources/GraphQL/Language/Visitor.swift index 7f428c2a..e1cc1fba 100644 --- a/Sources/GraphQL/Language/Visitor.swift +++ b/Sources/GraphQL/Language/Visitor.swift @@ -112,6 +112,7 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node node = parent parent = ancestors.popLast() + var parentEdited = false if isEdited { if inArray { var editOffset = 0 @@ -132,20 +133,22 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node } } else { if case let .node(n) = node { + var newNode = n for (editKey, editValue) in edits { if let editValue = editValue { if let key = editKey.keyValue { - n.set(value: .node(editValue), key: key) + newNode = newNode.set(value: .node(editValue), key: key) } } } - node = .node(n) + node = .node(newNode) } } // Since Swift cannot mutate node in-place, we must pass the changes up to parent. if let key = key, let node = node { parent = parent?.set(value: node, key: key) + parentEdited = true } } @@ -154,6 +157,11 @@ func visit(root: Node, visitor: Visitor, keyMap: [Kind: [String]] = [:]) -> Node edits = stack!.edits inArray = stack!.inArray stack = stack!.prev + // If parent has been edited, we must pass it up to the parent's parent and so on until + // we get to the root (at which point stack has no .prev) + if parentEdited, case let .node(parent) = parent, let stackPrev = stack?.prev { + edits.append((stackPrev.keys.last ?? "root", parent)) + } } else if let parent = parent { key = keys[index] node = parent.get(key: key!) diff --git a/Sources/GraphQL/Map/AnyCoder.swift b/Sources/GraphQL/Map/AnyCoder.swift index dcab5052..d12fba04 100644 --- a/Sources/GraphQL/Map/AnyCoder.swift +++ b/Sources/GraphQL/Map/AnyCoder.swift @@ -28,7 +28,7 @@ open class AnyEncoder { // MARK: Options /// The formatting of the output Any data. - public struct OutputFormatting: OptionSet { + public struct OutputFormatting: OptionSet, Sendable { /// The format's default value. public let rawValue: UInt @@ -41,7 +41,6 @@ open class AnyEncoder { public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) /// Produce Any with dictionary keys sorted in lexicographic order. - @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) } @@ -57,7 +56,6 @@ open class AnyEncoder { case millisecondsSince1970 /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Encode the `Date` as a string formatted by the given formatter. @@ -841,7 +839,7 @@ private extension _AnyEncoder { case .iso8601: if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - return NSString(string: _iso8601Formatter.string(from: date)) + return NSString(string: _iso8601Formatter().string(from: date)) } else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } @@ -1103,7 +1101,6 @@ open class AnyDecoder { case millisecondsSince1970 /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Decode the `Date` as a string parsed by the given formatter. @@ -3065,7 +3062,7 @@ private extension _AnyDecoder { case .iso8601: if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { let string = try self.unbox(value, as: String.self)! - guard let date = _iso8601Formatter.date(from: string) else { + guard let date = _iso8601Formatter().date(from: string) else { throw DecodingError.dataCorrupted(DecodingError.Context( codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted." @@ -3258,12 +3255,11 @@ private struct _AnyKey: CodingKey { //===----------------------------------------------------------------------===// // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -private var _iso8601Formatter: ISO8601DateFormatter = { +private func _iso8601Formatter() -> ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter -}() +} //===----------------------------------------------------------------------===// // Error Utilities diff --git a/Sources/GraphQL/Map/GraphQLJSONEncoder.swift b/Sources/GraphQL/Map/GraphQLJSONEncoder.swift index a938ee4f..e709b012 100644 --- a/Sources/GraphQL/Map/GraphQLJSONEncoder.swift +++ b/Sources/GraphQL/Map/GraphQLJSONEncoder.swift @@ -22,11 +22,11 @@ extension Dictionary: _JSONStringDictionaryEncodableMarker where Key == String, /// /// This is exactly the same as this `JSONEncoder` /// except with all Dictionary objects replaced with OrderedDictionary, and the name changed from JSONEncoder to GraphQLJSONEncoder -open class GraphQLJSONEncoder { +open class GraphQLJSONEncoder: @unchecked Sendable { // MARK: Options /// The formatting of the output JSON data. - public struct OutputFormatting: OptionSet { + public struct OutputFormatting: OptionSet, Sendable { /// The format's default value. public let rawValue: UInt @@ -39,7 +39,6 @@ open class GraphQLJSONEncoder { public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) /// Produce JSON with dictionary keys sorted in lexicographic order. - @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) /// By default slashes get escaped ("/" → "\/", "http://apple.com/" → "http:\/\/apple.com\/") @@ -61,7 +60,6 @@ open class GraphQLJSONEncoder { case millisecondsSince1970 /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Encode the `Date` as a string formatted by the given formatter. @@ -562,7 +560,7 @@ extension _SpecialTreatmentEncoder { case .iso8601: if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - return .string(_iso8601Formatter.string(from: date)) + return .string(_iso8601Formatter().string(from: date)) } else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } @@ -1298,12 +1296,11 @@ internal struct _JSONKey: CodingKey { //===----------------------------------------------------------------------===// // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -private var _iso8601Formatter: ISO8601DateFormatter = { +private func _iso8601Formatter() -> ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter -}() +} //===----------------------------------------------------------------------===// // Error Utilities diff --git a/Sources/GraphQL/Map/MapCoder.swift b/Sources/GraphQL/Map/MapCoder.swift index aabcac69..c36f7086 100644 --- a/Sources/GraphQL/Map/MapCoder.swift +++ b/Sources/GraphQL/Map/MapCoder.swift @@ -32,11 +32,11 @@ extension OrderedDictionary: _MapStringDictionaryDecodableMarker where Key == St //===----------------------------------------------------------------------===// /// `MapEncoder` facilitates the encoding of `Encodable` values into Map. -open class MapEncoder { +open class MapEncoder: @unchecked Sendable { // MARK: Options /// The formatting of the output Map data. - public struct OutputFormatting: OptionSet { + public struct OutputFormatting: OptionSet, Sendable { /// The format's default value. public let rawValue: UInt @@ -49,7 +49,6 @@ open class MapEncoder { public static let prettyPrinted = OutputFormatting(rawValue: 1 << 0) /// Produce Map with dictionary keys sorted in lexicographic order. - @available(macOS 10.13, iOS 11.0, watchOS 4.0, tvOS 11.0, *) public static let sortedKeys = OutputFormatting(rawValue: 1 << 1) } @@ -65,7 +64,6 @@ open class MapEncoder { case millisecondsSince1970 /// Encode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Encode the `Date` as a string formatted by the given formatter. @@ -849,7 +847,7 @@ private extension _MapEncoder { case .iso8601: if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { - return NSString(string: _iso8601Formatter.string(from: date)) + return NSString(string: _iso8601Formatter().string(from: date)) } else { fatalError("ISO8601DateFormatter is unavailable on this platform.") } @@ -1096,7 +1094,7 @@ private class _MapReferencingEncoder: _MapEncoder { //===----------------------------------------------------------------------===// /// `MapDecoder` facilitates the decoding of Map into semantic `Decodable` types. -open class MapDecoder { +open class MapDecoder: @unchecked Sendable { // MARK: Options /// The strategy to use for decoding `Date` values. @@ -1111,7 +1109,6 @@ open class MapDecoder { case millisecondsSince1970 /// Decode the `Date` as an ISO-8601-formatted string (in RFC 3339 format). - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// Decode the `Date` as a string parsed by the given formatter. @@ -3073,7 +3070,7 @@ private extension _MapDecoder { case .iso8601: if #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) { let string = try self.unbox(value, as: String.self)! - guard let date = _iso8601Formatter.date(from: string) else { + guard let date = _iso8601Formatter().date(from: string) else { throw DecodingError.dataCorrupted(DecodingError.Context( codingPath: self.codingPath, debugDescription: "Expected date string to be ISO8601-formatted." @@ -3266,12 +3263,11 @@ private struct _MapKey: CodingKey { //===----------------------------------------------------------------------===// // NOTE: This value is implicitly lazy and _must_ be lazy. We're compiled against the latest SDK (w/ ISO8601DateFormatter), but linked against whichever Foundation the user has. ISO8601DateFormatter might not exist, so we better not hit this code path on an older OS. -@available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) -private var _iso8601Formatter: ISO8601DateFormatter = { +private func _iso8601Formatter() -> ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter -}() +} //===----------------------------------------------------------------------===// // Error Utilities diff --git a/Sources/GraphQL/Map/Number.swift b/Sources/GraphQL/Map/Number.swift index f711cb27..d0459c8e 100644 --- a/Sources/GraphQL/Map/Number.swift +++ b/Sources/GraphQL/Map/Number.swift @@ -35,13 +35,11 @@ public struct Number: Sendable { storageType = .bool } - @available(OSX 10.5, *) public init(_ value: Int) { _number = NSNumber(value: value) storageType = .int } - @available(OSX 10.5, *) public init(_ value: UInt) { _number = NSNumber(value: value) storageType = .int @@ -101,12 +99,10 @@ public struct Number: Sendable { return _number.boolValue } - @available(OSX 10.5, *) public var intValue: Int { return _number.intValue } - @available(OSX 10.5, *) public var uintValue: UInt { return _number.uintValue } diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift deleted file mode 100644 index 5ff7ad89..00000000 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ /dev/null @@ -1,75 +0,0 @@ -/// Abstract event stream class - Should be overridden for actual implementations -open class EventStream { - public init() {} - /// Template method for mapping an event stream to a new generic type - MUST be overridden by - /// implementing types. - open func map(_: @escaping (Element) throws -> To) -> EventStream { - fatalError("This function should be overridden by implementing classes") - } -} - -/// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -public class ConcurrentEventStream: EventStream { - public let stream: AsyncThrowingStream - - 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) - } -} - -@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() - } - } - } - - 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.onTermination = { @Sendable _ in - task.cancel() - } - } - } -} diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 8236b3d5..56fed7da 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -1,79 +1,56 @@ -import NIO import OrderedCollections /** * Implements the "Subscribe" algorithm described in the GraphQL specification. * - * Returns a future which resolves to a SubscriptionResult containing either - * a SubscriptionObservable (if successful), or GraphQLErrors (error). + * Returns a `Result` that either succeeds with an `AsyncThrowingStream`, or fails with `GraphQLErrors`. * * If the client-provided arguments to this function do not result in a - * compliant subscription, the future will resolve to a - * SubscriptionResult containing `errors` and no `observable`. + * compliant subscription, the `Result` will fails with descriptive errors. * * If the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the future will resolve to a - * SubscriptionResult containing `errors` and no `observable`. + * resolver logic or underlying systems, the `Result` will fail with errors. * - * If the operation succeeded, the future will resolve to a SubscriptionResult, - * containing an `observable` which yields a stream of GraphQLResults + * If the operation succeeded, the `Result` will succeed with an `AsyncThrowingStream` of `GraphQLResult`s * representing the response stream. - * - * Accepts either an object with named arguments, or individual arguments. */ func subscribe( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, - eventLoopGroup: EventLoopGroup, + rootValue: any Sendable, + context: any Sendable, variableValues: [String: Map] = [:], operationName: String? = nil -) -> EventLoopFuture { - let sourceFuture = createSourceEventStream( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, +) async throws -> Result, GraphQLErrors> { + let sourceResult = try await createSourceEventStream( schema: schema, documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) - return sourceFuture.map { sourceResult -> SubscriptionResult in - if let sourceStream = sourceResult.stream { - let subscriptionStream = sourceStream.map { eventPayload -> Future in - // For each payload yielded from a subscription, map it over the normal - // GraphQL `execute` function, with `payload` as the rootValue. - // This implements the "MapSourceToResponseEvent" algorithm described in - // the GraphQL specification. The `execute` function provides the - // "ExecuteSubscriptionEvent" algorithm, as it is nearly identical to the - // "ExecuteQuery" algorithm, for which `execute` is also used. - execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - documentAST: documentAST, - rootValue: eventPayload, - context: context, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operationName: operationName - ) + return sourceResult.map { sourceStream in + AsyncThrowingStream { + // The type-cast below is required on Swift <6. Once we drop Swift 5 support it may be + // removed. + var iterator = sourceStream.makeAsyncIterator() as (any AsyncIteratorProtocol) + guard let eventPayload = try await iterator.next() else { + return nil } - return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) - } else { - return SubscriptionResult(errors: sourceResult.errors) + // Despite the warning, we must force unwrap because on optional unwrap, compiler + // throws: + // `marker protocol 'Sendable' cannot be used in a conditional cast` + let rootValue = eventPayload as! (any Sendable) + return try await execute( + schema: schema, + documentAST: documentAST, + rootValue: rootValue, + context: context, + variableValues: variableValues, + operationName: operationName + ) } } } @@ -82,20 +59,16 @@ func subscribe( * Implements the "CreateSourceEventStream" algorithm described in the * GraphQL specification, resolving the subscription source event stream. * - * Returns a Future which resolves to a SourceEventStreamResult, containing - * either an Observable (if successful) or GraphQLErrors (error). + * Returns a Result that either succeeds with an `AsyncSequence` or fails with `GraphQLErrors`. * * If the client-provided arguments to this function do not result in a - * compliant subscription, the future will resolve to a - * SourceEventStreamResult containing `errors` and no `observable`. + * compliant subscription, the `Result` will fail with descriptive errors. * * If the source stream could not be created due to faulty subscription - * resolver logic or underlying systems, the future will resolve to a - * SourceEventStreamResult containing `errors` and no `observable`. + * resolver logic or underlying systems, the `Result` will fail with errors. * - * If the operation succeeded, the future will resolve to a SubscriptionResult, - * containing an `observable` which yields a stream of event objects - * returned by the subscription resolver. + * If the operation succeeded, the `Result` will succeed with an AsyncSequence for the + * event stream returned by the resolver. * * A Source Event Stream represents a sequence of events, each of which triggers * a GraphQL execution for that event. @@ -106,64 +79,40 @@ func subscribe( * "Supporting Subscriptions at Scale" information in the GraphQL specification. */ func createSourceEventStream( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, - eventLoopGroup: EventLoopGroup, + rootValue: any Sendable, + context: any Sendable, variableValues: [String: Map] = [:], operationName: String? = nil -) -> EventLoopFuture { - let executeStarted = instrumentation.now - +) async throws -> Result { + // If a valid context cannot be created due to incorrect arguments, + // this will throw an error. + let exeContext = try buildExecutionContext( + schema: schema, + documentAST: documentAST, + rootValue: rootValue, + context: context, + rawVariableValues: variableValues, + operationName: operationName + ) do { - // If a valid context cannot be created due to incorrect arguments, - // this will throw an error. - let exeContext = try buildExecutionContext( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - documentAST: documentAST, - rootValue: rootValue, - context: context, - eventLoopGroup: eventLoopGroup, - rawVariableValues: variableValues, - operationName: operationName - ) - return try executeSubscription(context: exeContext, eventLoopGroup: eventLoopGroup) + return try await executeSubscription(context: exeContext) } catch let error as GraphQLError { - instrumentation.operationExecution( - processId: processId(), - threadId: threadId(), - started: executeStarted, - finished: instrumentation.now, - schema: schema, - document: documentAST, - rootValue: rootValue, - eventLoopGroup: eventLoopGroup, - variableValues: variableValues, - operation: nil, - errors: [error], - result: nil - ) - - return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult(errors: [error])) + // If it is a GraphQLError, report it as a failure. + return .failure(.init([error])) + } catch let errors as GraphQLErrors { + // If it is a GraphQLErrors, report it as a failure. + return .failure(errors) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(SourceEventStreamResult(errors: [GraphQLError(error)])) + // Otherwise treat the error as a system-class error and re-throw it. + throw error } } func executeSubscription( - context: ExecutionContext, - eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture { + context: ExecutionContext +) async throws -> Result { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) var inputFields: OrderedDictionary = [:] @@ -233,17 +182,16 @@ func executeSubscription( // Get the resolve func, regardless of if its result is normal // or abrupt (error). - let resolvedFutureOrError = resolveOrError( + let resolvedOrError = await resolveOrError( resolve: resolve, source: context.rootValue, args: args, context: contextValue, - eventLoopGroup: eventLoopGroup, info: info ) - let resolvedFuture: Future - switch resolvedFutureOrError { + let resolved: Any? + switch resolvedOrError { case let .failure(error): if let graphQLError = error as? GraphQLError { throw graphQLError @@ -251,40 +199,26 @@ func executeSubscription( throw GraphQLError(error) } case let .success(success): - resolvedFuture = success + resolved = success } - return resolvedFuture.map { resolved -> SourceEventStreamResult in - if !context.errors.isEmpty { - return SourceEventStreamResult(errors: context.errors) - } else if let error = resolved as? GraphQLError { - return SourceEventStreamResult(errors: [error]) - } else if let stream = resolved as? EventStream { - return SourceEventStreamResult(stream: stream) - } else if resolved == nil { - return SourceEventStreamResult(errors: [ - GraphQLError(message: "Resolved subscription was nil"), - ]) - } else { - let resolvedObj = resolved as AnyObject - return SourceEventStreamResult(errors: [ - GraphQLError( - message: "Subscription field resolver must return EventStream. Received: '\(resolvedObj)'" - ), - ]) - } - } -} - -// Subscription resolvers MUST return observables that are declared as 'Any' due to Swift not having -// covariant generic support for type -// checking. Normal resolvers for subscription fields should handle type casting, same as resolvers -// for query fields. -struct SourceEventStreamResult { - public let stream: EventStream? - public let errors: [GraphQLError] - - public init(stream: EventStream? = nil, errors: [GraphQLError] = []) { - self.stream = stream - self.errors = errors + if !context.errors.isEmpty { + return .failure(.init(context.errors)) + } else if let error = resolved as? GraphQLError { + return .failure(.init([error])) + } else if let stream = resolved as? any AsyncSequence { + // Despite the warning, we must force unwrap because on optional unwrap, compiler throws: + // `marker protocol 'Sendable' cannot be used in a conditional cast` + return .success(stream as! (any AsyncSequence & Sendable)) + } else if resolved == nil { + return .failure(.init([ + GraphQLError(message: "Resolved subscription was nil"), + ])) + } else { + let resolvedObj = resolved as AnyObject + return .failure(.init([ + GraphQLError( + message: "Subscription field resolver must return an AsyncSequence. Received: '\(resolvedObj)'" + ), + ])) } } diff --git a/Sources/GraphQL/SwiftUtilities/Mirror.swift b/Sources/GraphQL/SwiftUtilities/Mirror.swift index ec65931b..c6c26b49 100644 --- a/Sources/GraphQL/SwiftUtilities/Mirror.swift +++ b/Sources/GraphQL/SwiftUtilities/Mirror.swift @@ -1,4 +1,4 @@ -func unwrap(_ value: Any) -> Any? { +func unwrap(_ value: any Sendable) -> (any Sendable)? { let mirror = Mirror(reflecting: value) if mirror.displayStyle != .optional { @@ -9,14 +9,19 @@ func unwrap(_ value: Any) -> Any? { return nil } - return child.value + // Despite the warning, we must force unwrap because on optional unwrap, compiler throws: + // `marker protocol 'Sendable' cannot be used in a conditional cast` + return (child.value as! (any Sendable)) } extension Mirror { - func getValue(named key: String) -> Any? { + func getValue(named key: String) -> (any Sendable)? { guard let matched = children.filter({ $0.label == key }).first else { return nil } - return unwrap(matched.value) + + // Despite the warning, we must force unwrap because on optional unwrap, compiler throws: + // `marker protocol 'Sendable' cannot be used in a conditional cast` + return unwrap(matched.value as! (any Sendable)) } } diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 7f2c0924..20b15488 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -1,11 +1,10 @@ import Foundation -import NIO import OrderedCollections /** * These are all of the possible kinds of types. */ -public protocol GraphQLType: CustomDebugStringConvertible {} +public protocol GraphQLType: CustomDebugStringConvertible, Sendable {} extension GraphQLScalarType: GraphQLType {} extension GraphQLObjectType: GraphQLType {} extension GraphQLInterfaceType: GraphQLType {} @@ -158,7 +157,7 @@ extension GraphQLNonNull: GraphQLWrapperType {} * ) * */ -public final class GraphQLScalarType { +public final class GraphQLScalarType: Sendable { public let name: String public let description: String? public let specifiedByURL: String? @@ -166,17 +165,17 @@ public final class GraphQLScalarType { public let extensionASTNodes: [ScalarExtensionDefinition] public let kind: TypeKind = .scalar - let serialize: (Any) throws -> Map - let parseValue: (Map) throws -> Map - let parseLiteral: (Value) throws -> Map + let serialize: @Sendable (Any) throws -> Map + let parseValue: @Sendable (Map) throws -> Map + let parseLiteral: @Sendable (Value) throws -> Map public init( name: String, description: String? = nil, specifiedByURL: String? = nil, - serialize: @escaping (Any) throws -> Map = { try map(from: $0) }, - parseValue: ((Map) throws -> Map)? = nil, - parseLiteral: ((Value) throws -> Map)? = nil, + serialize: @escaping @Sendable (Any) throws -> Map = { try map(from: $0) }, + parseValue: (@Sendable (Map) throws -> Map)? = nil, + parseLiteral: (@Sendable (Value) throws -> Map)? = nil, astNode: ScalarTypeDefinition? = nil, extensionASTNodes: [ScalarExtensionDefinition] = [] ) throws { @@ -207,11 +206,11 @@ public final class GraphQLScalarType { } } -let defaultParseValue: ((Map) throws -> Map) = { value in +let defaultParseValue: (@Sendable (Map) throws -> Map) = { value in value } -let defaultParseLiteral: ((Value) throws -> Map) = { value in +let defaultParseLiteral: (@Sendable (Value) throws -> Map) = { value in try valueFromASTUntyped(valueAST: value) } @@ -273,9 +272,11 @@ extension GraphQLScalarType: Hashable { * ) * */ -public final class GraphQLObjectType { +public final class GraphQLObjectType: @unchecked Sendable { public let name: String public let description: String? + // While technically not sendable, fields and interfaces should not be mutated after schema + // creation. public var fields: () throws -> GraphQLFieldMap public var interfaces: () throws -> [GraphQLInterfaceType] public let isTypeOf: GraphQLIsTypeOf? @@ -407,39 +408,36 @@ extension String: TypeResolveResultRepresentable { } } -public enum TypeResolveResult { +public enum TypeResolveResult: Sendable { case type(GraphQLObjectType) case name(String) } -public typealias GraphQLTypeResolve = ( - _ value: Any, - _ eventLoopGroup: EventLoopGroup, +public typealias GraphQLTypeResolve = @Sendable ( + _ value: any Sendable, _ info: GraphQLResolveInfo ) throws -> TypeResolveResultRepresentable -public typealias GraphQLIsTypeOf = ( - _ source: Any, - _ eventLoopGroup: EventLoopGroup, +public typealias GraphQLIsTypeOf = @Sendable ( + _ source: any Sendable, _ info: GraphQLResolveInfo ) throws -> Bool -public typealias GraphQLFieldResolve = ( - _ source: Any, +public typealias GraphQLFieldResolve = @Sendable ( + _ source: any Sendable, _ args: Map, - _ context: Any, - _ eventLoopGroup: EventLoopGroup, + _ context: any Sendable, _ info: GraphQLResolveInfo -) throws -> Future +) async throws -> (any Sendable)? -public typealias GraphQLFieldResolveInput = ( - _ source: Any, +public typealias GraphQLFieldResolveInput = @Sendable ( + _ source: any Sendable, _ args: Map, - _ context: Any, + _ context: any Sendable, _ info: GraphQLResolveInfo -) throws -> Any? +) throws -> (any Sendable)? -public struct GraphQLResolveInfo { +public struct GraphQLResolveInfo: Sendable { public let fieldName: String public let fieldASTs: [Field] public let returnType: GraphQLOutputType @@ -447,14 +445,14 @@ public struct GraphQLResolveInfo { public let path: IndexPath public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] - public let rootValue: Any + public let rootValue: any Sendable public let operation: OperationDefinition - public let variableValues: [String: Any] + public let variableValues: [String: any Sendable] } public typealias GraphQLFieldMap = OrderedDictionary -public struct GraphQLField { +public struct GraphQLField: Sendable { public let type: GraphQLOutputType public let args: GraphQLArgumentConfigMap public let deprecationReason: String? @@ -511,9 +509,9 @@ public struct GraphQLField { self.description = description self.astNode = astNode - self.resolve = { source, args, context, eventLoopGroup, info in + self.resolve = { source, args, context, info in let result = try resolve(source, args, context, info) - return eventLoopGroup.next().makeSucceededFuture(result) + return result } subscribe = nil } @@ -521,10 +519,10 @@ public struct GraphQLField { public typealias GraphQLFieldDefinitionMap = OrderedDictionary -public final class GraphQLFieldDefinition { +public final class GraphQLFieldDefinition: Sendable { public let name: String public let description: String? - public internal(set) var type: GraphQLOutputType + public let type: GraphQLOutputType public let args: [GraphQLArgumentDefinition] public let resolve: GraphQLFieldResolve? public let subscribe: GraphQLFieldResolve? @@ -576,7 +574,7 @@ public final class GraphQLFieldDefinition { public typealias GraphQLArgumentConfigMap = OrderedDictionary -public struct GraphQLArgument { +public struct GraphQLArgument: Sendable { public let type: GraphQLInputType public let description: String? public let defaultValue: Map? @@ -598,7 +596,7 @@ public struct GraphQLArgument { } } -public struct GraphQLArgumentDefinition { +public struct GraphQLArgumentDefinition: Sendable { public let name: String public let type: GraphQLInputType public let defaultValue: Map? @@ -655,10 +653,12 @@ public func isRequiredArgument(_ arg: GraphQLArgumentDefinition) -> Bool { * ) * */ -public final class GraphQLInterfaceType { +public final class GraphQLInterfaceType: @unchecked Sendable { public let name: String public let description: String? public let resolveType: GraphQLTypeResolve? + // While technically not sendable, fields and interfaces should not be mutated after schema + // creation. public var fields: () throws -> GraphQLFieldMap public var interfaces: () throws -> [GraphQLInterfaceType] public let astNode: InterfaceTypeDefinition? @@ -758,12 +758,13 @@ public typealias GraphQLUnionTypeExtensions = [String: String]? * ) * */ -public final class GraphQLUnionType { +public final class GraphQLUnionType: @unchecked Sendable { public let kind: TypeKind = .union public let name: String public let description: String? public let resolveType: GraphQLTypeResolve? - public let types: () throws -> [GraphQLObjectType] + // While technically not sendable, types should not be mutated after schema creation. + public internal(set) var types: () throws -> [GraphQLObjectType] public let possibleTypeNames: [String: Bool] let extensions: [GraphQLUnionTypeExtensions] let astNode: UnionTypeDefinition? @@ -857,7 +858,7 @@ extension GraphQLUnionType: Hashable { * Note: If a value is not provided in a definition, the name of the enum value * will be used as its internal value. */ -public final class GraphQLEnumType { +public final class GraphQLEnumType: Sendable { public let name: String public let description: String? public let values: [GraphQLEnumValueDefinition] @@ -1009,7 +1010,7 @@ public struct GraphQLEnumValue { } } -public struct GraphQLEnumValueDefinition { +public struct GraphQLEnumValueDefinition: Sendable { public let name: String public let description: String? public let deprecationReason: String? @@ -1054,9 +1055,10 @@ public struct GraphQLEnumValueDefinition { * ) * */ -public final class GraphQLInputObjectType { +public final class GraphQLInputObjectType: @unchecked Sendable { public let name: String public let description: String? + // While technically not sendable, this should not be mutated after schema creation. public var fields: () throws -> InputObjectFieldMap public let astNode: InputObjectTypeDefinition? public let extensionASTNodes: [InputObjectExtensionDefinition] @@ -1147,7 +1149,7 @@ func defineInputObjectFieldMap( return definitionMap } -public struct InputObjectField { +public struct InputObjectField: Sendable { public let type: GraphQLInputType public let defaultValue: Map? public let description: String? @@ -1171,9 +1173,9 @@ public struct InputObjectField { public typealias InputObjectFieldMap = OrderedDictionary -public final class InputObjectFieldDefinition { +public final class InputObjectFieldDefinition: Sendable { public let name: String - public internal(set) var type: GraphQLInputType + public let type: GraphQLInputType public let description: String? public let defaultValue: Map? public let deprecationReason: String? diff --git a/Sources/GraphQL/Type/Directives.swift b/Sources/GraphQL/Type/Directives.swift index ffc0b92e..b4844027 100644 --- a/Sources/GraphQL/Type/Directives.swift +++ b/Sources/GraphQL/Type/Directives.swift @@ -1,6 +1,6 @@ import OrderedCollections -public enum DirectiveLocation: String, Encodable { +public enum DirectiveLocation: String, Encodable, Sendable { // Operations case query = "QUERY" case mutation = "MUTATION" @@ -29,7 +29,7 @@ public enum DirectiveLocation: String, Encodable { * Directives are used by the GraphQL runtime as a way of modifying execution * behavior. Type system creators will usually not create these directly. */ -public final class GraphQLDirective { +public final class GraphQLDirective: Sendable { public let name: String public let description: String? public let locations: [DirectiveLocation] diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index 9841851d..185aba08 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -1,4 +1,3 @@ -import NIO let __Schema = try! GraphQLObjectType( name: "__Schema", @@ -84,7 +83,9 @@ let __Directive = try! GraphQLObjectType( "name": GraphQLField(type: GraphQLNonNull(GraphQLString)), "description": GraphQLField(type: GraphQLString), "isRepeatable": GraphQLField(type: GraphQLNonNull(GraphQLBoolean)), - "locations": GraphQLField(type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation)))), + "locations": GraphQLField( + type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__DirectiveLocation))) + ), "args": GraphQLField( type: GraphQLNonNull(GraphQLList(GraphQLNonNull(__InputValue))), args: [ @@ -201,8 +202,7 @@ let __Type: GraphQLObjectType = { "beyond a name, description and optional \\`specifiedByURL\\`, while Enum types provide their values. " + "Object and Interface types provide the fields they describe. Abstract " + "types, Union and Interface, provide the Object types possible " + - "at runtime. List and NonNull types compose other types.", - fields: [:] + "at runtime. List and NonNull types compose other types." ) __Type.fields = { [ "kind": GraphQLField( @@ -426,7 +426,7 @@ let __EnumValue = try! GraphQLObjectType( ] ) -public enum TypeKind: String, Encodable { +public enum TypeKind: String, Encodable, Sendable { case scalar = "SCALAR" case object = "OBJECT" case interface = "INTERFACE" @@ -492,8 +492,8 @@ let SchemaMetaFieldDef = GraphQLFieldDefinition( name: "__schema", type: GraphQLNonNull(__Schema), description: "Access the current type schema of this server.", - resolve: { _, _, _, eventLoopGroup, info in - eventLoopGroup.next().makeSucceededFuture(info.schema) + resolve: { _, _, _, info in + info.schema } ) @@ -507,9 +507,9 @@ let TypeMetaFieldDef = GraphQLFieldDefinition( type: GraphQLNonNull(GraphQLString) ), ], - resolve: { _, arguments, _, eventLoopGroup, info in + resolve: { _, arguments, _, info in let name = arguments["name"].string! - return eventLoopGroup.next().makeSucceededFuture(info.schema.getType(name: name)) + return info.schema.getType(name: name) } ) @@ -517,8 +517,8 @@ let TypeNameMetaFieldDef = GraphQLFieldDefinition( name: "__typename", type: GraphQLNonNull(GraphQLString), description: "The name of the current Object type at runtime.", - resolve: { _, _, _, eventLoopGroup, info in - eventLoopGroup.next().makeSucceededFuture(info.parentType.name) + resolve: { _, _, _, info in + info.parentType.name } ) diff --git a/Sources/GraphQL/Type/Schema.swift b/Sources/GraphQL/Type/Schema.swift index 3b06b455..969c41f5 100644 --- a/Sources/GraphQL/Type/Schema.swift +++ b/Sources/GraphQL/Type/Schema.swift @@ -1,3 +1,4 @@ +import Dispatch import OrderedCollections /** @@ -27,22 +28,60 @@ import OrderedCollections * ) * */ -public final class GraphQLSchema { +public final class GraphQLSchema: @unchecked Sendable { let description: String? let extensions: [GraphQLSchemaExtensions] let astNode: SchemaDefinition? let extensionASTNodes: [SchemaExtensionDefinition] // Used as a cache for validateSchema(). - var validationErrors: [GraphQLError]? + private var _validationErrors: [GraphQLError]? + private let validationErrorQueue = DispatchQueue( + label: "graphql.schema.validationerrors", + attributes: .concurrent + ) + var validationErrors: [GraphQLError]? { + get { + // Reads can occur concurrently. + return validationErrorQueue.sync { + _validationErrors + } + } + set { + // Writes occur sequentially. + return validationErrorQueue.async(flags: .barrier) { + self._validationErrors = newValue + } + } + } public let queryType: GraphQLObjectType? public let mutationType: GraphQLObjectType? public let subscriptionType: GraphQLObjectType? public let directives: [GraphQLDirective] public let typeMap: TypeMap - public internal(set) var implementations: [String: InterfaceImplementations] - private var subTypeMap: [String: [String: Bool]] = [:] + public let implementations: [String: InterfaceImplementations] + + // Used as a cache for validateSchema(). + private var _subTypeMap: [String: [String: Bool]] = [:] + private let subTypeMapQueue = DispatchQueue( + label: "graphql.schema.subtypeMap", + attributes: .concurrent + ) + var subTypeMap: [String: [String: Bool]] { + get { + // Reads can occur concurrently. + return subTypeMapQueue.sync { + _subTypeMap + } + } + set { + // Writes occur sequentially. + return subTypeMapQueue.async(flags: .barrier) { + self._subTypeMap = newValue + } + } + } public init( description: String? = nil, @@ -56,7 +95,7 @@ public final class GraphQLSchema { extensionASTNodes: [SchemaExtensionDefinition] = [], assumeValid: Bool = false ) throws { - validationErrors = assumeValid ? [] : nil + _validationErrors = assumeValid ? [] : nil self.description = description self.extensions = extensions @@ -109,7 +148,38 @@ public final class GraphQLSchema { var typeMap = TypeMap() // Keep track of all implementations by interface name. - implementations = try collectImplementations(types: Array(typeMap.values)) + func collectImplementations( + types: [GraphQLNamedType] + ) throws -> [String: InterfaceImplementations] { + var implementations: [String: InterfaceImplementations] = [:] + + for type in types { + if let type = type as? GraphQLInterfaceType { + if implementations[type.name] == nil { + implementations[type.name] = InterfaceImplementations() + } + + // Store implementations by interface. + for iface in try type.getInterfaces() { + implementations[iface.name] = InterfaceImplementations( + interfaces: (implementations[iface.name]?.interfaces ?? []) + [type] + ) + } + } + + if let type = type as? GraphQLObjectType { + // Store implementations by objects. + for iface in try type.getInterfaces() { + implementations[iface.name] = InterfaceImplementations( + objects: (implementations[iface.name]?.objects ?? []) + [type] + ) + } + } + } + + return implementations + } + var implementations = try collectImplementations(types: Array(typeMap.values)) for namedType in allReferencedTypes.values { let typeName = namedType.name @@ -124,37 +194,38 @@ public final class GraphQLSchema { if let namedType = namedType as? GraphQLInterfaceType { // Store implementations by interface. for iface in try namedType.getInterfaces() { - let implementations = self.implementations[iface.name] ?? .init( + let implementation = implementations[iface.name] ?? .init( objects: [], interfaces: [] ) - var interfaces = implementations.interfaces + var interfaces = implementation.interfaces interfaces.append(namedType) - self.implementations[iface.name] = .init( - objects: implementations.objects, + implementations[iface.name] = .init( + objects: implementation.objects, interfaces: interfaces ) } } else if let namedType = namedType as? GraphQLObjectType { // Store implementations by objects. for iface in try namedType.getInterfaces() { - let implementations = self.implementations[iface.name] ?? .init( + let implementation = implementations[iface.name] ?? .init( objects: [], interfaces: [] ) - var objects = implementations.objects + var objects = implementation.objects objects.append(namedType) - self.implementations[iface.name] = .init( + implementations[iface.name] = .init( objects: objects, - interfaces: implementations.interfaces + interfaces: implementation.interfaces ) } } } self.typeMap = typeMap + self.implementations = implementations } convenience init(config: GraphQLSchemaNormalizedConfig) throws { @@ -268,7 +339,7 @@ public final class GraphQLSchema { public typealias TypeMap = OrderedDictionary -public struct InterfaceImplementations { +public struct InterfaceImplementations: Sendable { public let objects: [GraphQLObjectType] public let interfaces: [GraphQLInterfaceType] @@ -281,38 +352,6 @@ public struct InterfaceImplementations { } } -func collectImplementations( - types: [GraphQLNamedType] -) throws -> [String: InterfaceImplementations] { - var implementations: [String: InterfaceImplementations] = [:] - - for type in types { - if let type = type as? GraphQLInterfaceType { - if implementations[type.name] == nil { - implementations[type.name] = InterfaceImplementations() - } - - // Store implementations by interface. - for iface in try type.getInterfaces() { - implementations[iface.name] = InterfaceImplementations( - interfaces: (implementations[iface.name]?.interfaces ?? []) + [type] - ) - } - } - - if let type = type as? GraphQLObjectType { - // Store implementations by objects. - for iface in try type.getInterfaces() { - implementations[iface.name] = InterfaceImplementations( - objects: (implementations[iface.name]?.objects ?? []) + [type] - ) - } - } - } - - return implementations -} - func typeMapReducer(typeMap: TypeMap, type: GraphQLType) throws -> TypeMap { var typeMap = typeMap diff --git a/Sources/GraphQL/Type/Validation.swift b/Sources/GraphQL/Type/Validation.swift index 86589d9c..af863826 100644 --- a/Sources/GraphQL/Type/Validation.swift +++ b/Sources/GraphQL/Type/Validation.swift @@ -5,7 +5,7 @@ * Validation runs synchronously, returning an array of encountered errors, or * an empty array if no errors were encountered and the Schema is valid. */ -func validateSchema( +public func validateSchema( schema: GraphQLSchema ) throws -> [GraphQLError] { // If this Schema has already been validated, return the previous results. diff --git a/Sources/GraphQL/Utilities/ExtendSchema.swift b/Sources/GraphQL/Utilities/ExtendSchema.swift index 35f64e09..acca734c 100644 --- a/Sources/GraphQL/Utilities/ExtendSchema.swift +++ b/Sources/GraphQL/Utilities/ExtendSchema.swift @@ -403,7 +403,7 @@ func extendSchemaImpl( ) } - struct OperationTypes { + struct OperationTypes: Sendable { let query: GraphQLObjectType? let mutation: GraphQLObjectType? let subscription: GraphQLObjectType? diff --git a/Sources/GraphQL/Utilities/Keyable.swift b/Sources/GraphQL/Utilities/Keyable.swift index a7bd7459..5246f0f1 100644 --- a/Sources/GraphQL/Utilities/Keyable.swift +++ b/Sources/GraphQL/Utilities/Keyable.swift @@ -1,3 +1,3 @@ public protocol KeySubscriptable { - subscript(_: String) -> Any? { get } + subscript(_: String) -> (any Sendable)? { get } } diff --git a/Sources/GraphQL/Utilities/NIO+Extensions.swift b/Sources/GraphQL/Utilities/NIO+Extensions.swift deleted file mode 100644 index 2131551a..00000000 --- a/Sources/GraphQL/Utilities/NIO+Extensions.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// NIO+Extensions.swift -// GraphQL -// -// Created by Jeff Seibert on 3/9/18. -// - -import Foundation -import NIO -import OrderedCollections - -public typealias Future = EventLoopFuture - -public extension Collection { - func flatten(on eventLoopGroup: EventLoopGroup) -> Future<[T]> where Element == Future { - return Future.whenAllSucceed(Array(self), on: eventLoopGroup.next()) - } -} - -extension Dictionary where Value: FutureType { - func flatten(on eventLoopGroup: EventLoopGroup) -> Future<[Key: Value.Expectation]> { - // create array of futures with (key,value) tuple - let futures: [Future<(Key, Value.Expectation)>] = map { element in - element.value.map(file: #file, line: #line) { (key: element.key, value: $0) } - } - // when all futures have succeeded convert tuple array back to dictionary - return EventLoopFuture.whenAllSucceed(futures, on: eventLoopGroup.next()).map { - .init(uniqueKeysWithValues: $0) - } - } -} - -extension OrderedDictionary where Value: FutureType { - func flatten(on eventLoopGroup: EventLoopGroup) - -> Future> - { - let keys = self.keys - // create array of futures with (key,value) tuple - let futures: [Future<(Key, Value.Expectation)>] = map { element in - element.value.map(file: #file, line: #line) { (key: element.key, value: $0) } - } - // when all futures have succeeded convert tuple array back to dictionary - return EventLoopFuture.whenAllSucceed(futures, on: eventLoopGroup.next()) - .map { unorderedResult in - var result: OrderedDictionary = [:] - for key in keys { - // Unwrap is guaranteed because keys are from original dictionary and maps - // preserve all elements - result[key] = unorderedResult.first(where: { $0.0 == key })!.1 - } - return result - } - } -} - -public protocol FutureType { - associatedtype Expectation - func whenSuccess(_ callback: @escaping @Sendable (Expectation) -> Void) - func whenFailure(_ callback: @escaping @Sendable (Error) -> Void) - func map( - file: StaticString, - line: UInt, - _ callback: @escaping (Expectation) -> (NewValue) - ) -> EventLoopFuture -} - -extension Future: FutureType { - public typealias Expectation = Value -} - -// Copied from https://github.com/vapor/async-kit/blob/e2f741640364c1d271405da637029ea6a33f754e/Sources/AsyncKit/EventLoopFuture/Future%2BTry.swift -// in order to avoid full package dependency. -public extension EventLoopFuture { - func tryFlatMap( - file _: StaticString = #file, line _: UInt = #line, - _ callback: @escaping (Value) throws -> EventLoopFuture - ) -> EventLoopFuture { - /// When the current `EventLoopFuture` is fulfilled, run the provided callback, - /// which will provide a new `EventLoopFuture`. - /// - /// This allows you to dynamically dispatch new asynchronous tasks as phases in a - /// longer series of processing steps. Note that you can use the results of the - /// current `EventLoopFuture` when determining how to dispatch the next operation. - /// - /// The key difference between this method and the regular `flatMap` is error handling. - /// - /// With `tryFlatMap`, the provided callback _may_ throw Errors, causing the returned - /// `EventLoopFuture` - /// to report failure immediately after the completion of the original `EventLoopFuture`. - flatMap { [eventLoop] value in - do { - return try callback(value) - } catch { - return eventLoop.makeFailedFuture(error) - } - } - } -} diff --git a/Sources/GraphQL/Validation/SpecifiedRules.swift b/Sources/GraphQL/Validation/SpecifiedRules.swift index fe46dd29..473748e1 100644 --- a/Sources/GraphQL/Validation/SpecifiedRules.swift +++ b/Sources/GraphQL/Validation/SpecifiedRules.swift @@ -1,7 +1,7 @@ /** * This set includes all validation rules defined by the GraphQL spec. */ -public let specifiedRules: [(ValidationContext) -> Visitor] = [ +public let specifiedRules: [@Sendable (ValidationContext) -> Visitor] = [ ExecutableDefinitionsRule, UniqueOperationNamesRule, LoneAnonymousOperationRule, diff --git a/Sources/GraphQL/Validation/Validate.swift b/Sources/GraphQL/Validation/Validate.swift index 362d4ad4..27e72602 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -1,22 +1,3 @@ -/// Implements the "Validation" section of the spec. -/// -/// Validation runs synchronously, returning an array of encountered errors, or -/// an empty array if no errors were encountered and the document is valid. -/// -/// - Parameters: -/// - instrumentation: The instrumentation implementation to call during the parsing, validating, -/// execution, and field resolution stages. -/// - schema: The GraphQL type system to use when validating and executing a query. -/// - ast: A GraphQL document representing the requested operation. -/// - Returns: zero or more errors -public func validate( - instrumentation: Instrumentation = NoOpInstrumentation, - schema: GraphQLSchema, - ast: Document -) -> [GraphQLError] { - return validate(instrumentation: instrumentation, schema: schema, ast: ast, rules: []) -} - /** * Implements the "Validation" section of the spec. * @@ -31,24 +12,13 @@ public func validate( * GraphQLErrors, or Arrays of GraphQLErrors when invalid. */ public func validate( - instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, ast: Document, - rules: [(ValidationContext) -> Visitor] + rules: [@Sendable (ValidationContext) -> Visitor] = specifiedRules ) -> [GraphQLError] { - let started = instrumentation.now let typeInfo = TypeInfo(schema: schema) let rules = rules.isEmpty ? specifiedRules : rules let errors = visit(usingRules: rules, schema: schema, typeInfo: typeInfo, documentAST: ast) - instrumentation.queryValidation( - processId: processId(), - threadId: threadId(), - started: started, - finished: instrumentation.now, - schema: schema, - document: ast, - errors: errors - ) return errors } @@ -82,7 +52,7 @@ func validateSDL( * @internal */ func visit( - usingRules rules: [(ValidationContext) -> Visitor], + usingRules rules: [@Sendable (ValidationContext) -> Visitor], schema: GraphQLSchema, typeInfo: TypeInfo, documentAST: Document diff --git a/Sources/GraphQL/Validation/ValidationContext.swift b/Sources/GraphQL/Validation/ValidationContext.swift index 6732a9c8..708835e4 100644 --- a/Sources/GraphQL/Validation/ValidationContext.swift +++ b/Sources/GraphQL/Validation/ValidationContext.swift @@ -137,7 +137,7 @@ public class ASTValidationContext { } } -typealias ValidationRule = (ValidationContext) -> Visitor +typealias ValidationRule = @Sendable (ValidationContext) -> Visitor public class SDLValidationContext: ASTValidationContext { public let schema: GraphQLSchema? @@ -160,7 +160,7 @@ public class SDLValidationContext: ASTValidationContext { } } -public typealias SDLValidationRule = (SDLValidationContext) -> Visitor +public typealias SDLValidationRule = @Sendable (SDLValidationContext) -> Visitor /** * An instance of this class is passed as the "this" context to all validators, diff --git a/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift b/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift index 1b6a50d9..8c4bf25b 100644 --- a/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift +++ b/Tests/GraphQLTests/ExecutionTests/OneOfTests.swift @@ -1,13 +1,10 @@ @testable import GraphQL -import NIO import XCTest class OneOfTests: XCTestCase { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - // MARK: OneOf Input Objects - func testAcceptsAGoodDefaultValue() throws { + func testAcceptsAGoodDefaultValue() async throws { let query = """ query ($input: TestInputObject! = {a: "abc"}) { test(input: $input) { @@ -16,11 +13,10 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual( result, GraphQLResult(data: [ @@ -32,7 +28,7 @@ class OneOfTests: XCTestCase { ) } - func testRejectsABadDefaultValue() throws { + func testRejectsABadDefaultValue() async throws { let query = """ query ($input: TestInputObject! = {a: "abc", b: 123}) { test(input: $input) { @@ -41,11 +37,10 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result.errors.count, 1) XCTAssertEqual( result.errors[0].message, @@ -53,7 +48,7 @@ class OneOfTests: XCTestCase { ) } - func testAcceptsAGoodVariable() throws { + func testAcceptsAGoodVariable() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -62,12 +57,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc"]] - ).wait() + ) XCTAssertEqual( result, GraphQLResult(data: [ @@ -79,7 +73,7 @@ class OneOfTests: XCTestCase { ) } - func testAcceptsAGoodVariableWithAnUndefinedKey() throws { + func testAcceptsAGoodVariableWithAnUndefinedKey() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -88,12 +82,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc", "b": .undefined]] - ).wait() + ) XCTAssertEqual( result, GraphQLResult(data: [ @@ -105,7 +98,7 @@ class OneOfTests: XCTestCase { ) } - func testRejectsAVariableWithMultipleNonNullKeys() throws { + func testRejectsAVariableWithMultipleNonNullKeys() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -114,12 +107,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc", "b": 123]] - ).wait() + ) XCTAssertEqual(result.errors.count, 1) XCTAssertEqual( result.errors[0].message, @@ -130,7 +122,7 @@ class OneOfTests: XCTestCase { ) } - func testRejectsAVariableWithMultipleNullableKeys() throws { + func testRejectsAVariableWithMultipleNullableKeys() async throws { let query = """ query ($input: TestInputObject!) { test(input: $input) { @@ -139,12 +131,11 @@ class OneOfTests: XCTestCase { } } """ - let result = try graphql( + let result = try await graphql( schema: getSchema(), request: query, - eventLoopGroup: eventLoopGroup, variableValues: ["input": ["a": "abc", "b": .null]] - ).wait() + ) XCTAssertEqual(result.errors.count, 1) XCTAssertEqual( result.errors[0].message, @@ -163,7 +154,7 @@ func getSchema() throws -> GraphQLSchema { "a": GraphQLField(type: GraphQLString), "b": GraphQLField(type: GraphQLInt), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is TestObject } ) diff --git a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift deleted file mode 100644 index cd021f3b..00000000 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ /dev/null @@ -1,316 +0,0 @@ -import Dispatch -@testable import GraphQL -import NIO -import XCTest - -class FieldExecutionStrategyTests: XCTestCase { - enum StrategyError: Error { - case exampleError(msg: String) - } - - let schema = try! GraphQLSchema( - query: GraphQLObjectType( - name: "RootQueryType", - fields: [ - "sleep": GraphQLField( - type: GraphQLString, - resolve: { _, _, _, eventLoopGroup, _ in - eventLoopGroup.next().makeSucceededVoidFuture().map { - Thread.sleep(forTimeInterval: 0.1) - return "z" - } - } - ), - "bang": GraphQLField( - type: GraphQLString, - resolve: { (_, _, _, _, info: GraphQLResolveInfo) in - let group = DispatchGroup() - group.enter() - - DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) { - group.leave() - } - - group.wait() - - throw StrategyError.exampleError( - msg: "\(info.fieldName): \(info.path.elements.last!)" - ) - } - ), - "futureBang": GraphQLField( - type: GraphQLString, - resolve: { (_, _, _, eventLoopGroup, info: GraphQLResolveInfo) in - let g = DispatchGroup() - g.enter() - - DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) { - g.leave() - } - - g.wait() - - return eventLoopGroup.next().makeFailedFuture(StrategyError.exampleError( - msg: "\(info.fieldName): \(info.path.elements.last!)" - )) - } - ), - ] - ) - ) - - let singleQuery = "{ sleep }" - - let singleExpected = GraphQLResult( - data: [ - "sleep": "z", - ] - ) - - let multiQuery = - "{ a: sleep b: sleep c: sleep d: sleep e: sleep f: sleep g: sleep h: sleep i: sleep j: sleep }" - - let multiExpected = GraphQLResult( - data: [ - "a": "z", - "b": "z", - "c": "z", - "d": "z", - "e": "z", - "f": "z", - "g": "z", - "h": "z", - "i": "z", - "j": "z", - ] - ) - - let singleThrowsQuery = "{ bang }" - - let singleThrowsExpected = GraphQLResult( - data: [ - "bang": nil, - ], - errors: [ - GraphQLError( - message: "exampleError(msg: \"bang: bang\")", - locations: [SourceLocation(line: 1, column: 3)], - path: ["bang"] - ), - ] - ) - - let singleFailedFutureQuery = "{ futureBang }" - - let singleFailedFutureExpected = GraphQLResult( - data: [ - "futureBang": nil, - ], - errors: [ - GraphQLError( - message: "exampleError(msg: \"futureBang: futureBang\")", - locations: [SourceLocation(line: 1, column: 3)], - path: ["futureBang"] - ), - ] - ) - - let multiThrowsQuery = - "{ a: bang b: bang c: bang d: bang e: bang f: bang g: bang h: bang i: bang j: futureBang }" - - let multiThrowsExpectedData: Map = [ - "a": nil, - "b": nil, - "c": nil, - "d": nil, - "e": nil, - "f": nil, - "g": nil, - "h": nil, - "i": nil, - "j": nil, - ] - - let multiThrowsExpectedErrors: [GraphQLError] = [ - GraphQLError( - message: "exampleError(msg: \"bang: a\")", - locations: [SourceLocation(line: 1, column: 3)], - path: ["a"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: b\")", - locations: [SourceLocation(line: 1, column: 11)], - path: ["b"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: c\")", - locations: [SourceLocation(line: 1, column: 19)], - path: ["c"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: d\")", - locations: [SourceLocation(line: 1, column: 27)], - path: ["d"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: e\")", - locations: [SourceLocation(line: 1, column: 35)], - path: ["e"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: f\")", - locations: [SourceLocation(line: 1, column: 43)], - path: ["f"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: g\")", - locations: [SourceLocation(line: 1, column: 51)], - path: ["g"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: h\")", - locations: [SourceLocation(line: 1, column: 59)], - path: ["h"] - ), - GraphQLError( - message: "exampleError(msg: \"bang: i\")", - locations: [SourceLocation(line: 1, column: 67)], - path: ["i"] - ), - GraphQLError( - message: "exampleError(msg: \"futureBang: j\")", - locations: [SourceLocation(line: 1, column: 75)], - path: ["j"] - ), - ] - - func timing(_ block: @autoclosure () throws -> T) throws -> (value: T, seconds: Double) { - let start = DispatchTime.now() - let value = try block() - let nanoseconds = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds - let seconds = Double(nanoseconds) / 1_000_000_000 - return ( - value: value, - seconds: seconds - ) - } - - private var eventLoopGroup: EventLoopGroup! - - override func setUp() { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - override func tearDown() { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - - func testSerialFieldExecutionStrategyWithSingleField() throws { - let result = try timing(graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: singleQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, singleExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testSerialFieldExecutionStrategyWithSingleFieldError() throws { - let result = try timing(graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: singleThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, singleThrowsExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testSerialFieldExecutionStrategyWithSingleFieldFailedFuture() throws { - let result = try timing(graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: singleFailedFutureQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, singleFailedFutureExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testSerialFieldExecutionStrategyWithMultipleFields() throws { - let result = try timing(graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: multiQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, multiExpected) -// XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) - } - - func testSerialFieldExecutionStrategyWithMultipleFieldErrors() throws { - let result = try timing(graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: multiThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value.data, multiThrowsExpectedData) - let resultErrors = result.value.errors - XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) - for m in multiThrowsExpectedErrors { - XCTAssertTrue(resultErrors.contains(m), "Expecting result errors to contain \(m)") - } - // XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) - } - - func testConcurrentDispatchFieldExecutionStrategyWithSingleField() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), - schema: schema, - request: singleQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, singleExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testConcurrentDispatchFieldExecutionStrategyWithSingleFieldError() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), - schema: schema, - request: singleThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, singleThrowsExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testConcurrentDispatchFieldExecutionStrategyWithMultipleFields() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), - schema: schema, - request: multiQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value, multiExpected) -// XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testConcurrentDispatchFieldExecutionStrategyWithMultipleFieldErrors() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), - schema: schema, - request: multiThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) - XCTAssertEqual(result.value.data, multiThrowsExpectedData) - let resultErrors = result.value.errors - XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) - for m in multiThrowsExpectedErrors { - XCTAssertTrue(resultErrors.contains(m), "Expecting result errors to contain \(m)") - } - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } -} diff --git a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift index de6252ea..3131c600 100644 --- a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO import XCTest class HelloWorldTests: XCTestCase { @@ -17,32 +16,19 @@ class HelloWorldTests: XCTestCase { ) ) - func testHello() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - + func testHello() async throws { let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) - let result = try graphql( + let result = try await graphql( schema: schema, - request: query, - eventLoopGroup: group - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testBoyhowdy() throws { - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - + func testBoyhowdy() async throws { let query = "{ boyhowdy }" let expected = GraphQLResult( @@ -54,30 +40,21 @@ class HelloWorldTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: schema, - request: query, - eventLoopGroup: group - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - @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()) - } - let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) let result = try await graphql( schema: schema, - request: query, - eventLoopGroup: group + request: query ) XCTAssertEqual(result, expected) diff --git a/Tests/GraphQLTests/InputTests/InputTests.swift b/Tests/GraphQLTests/InputTests/InputTests.swift index fe66d461..0c9fd2b7 100644 --- a/Tests/GraphQLTests/InputTests/InputTests.swift +++ b/Tests/GraphQLTests/InputTests/InputTests.swift @@ -1,9 +1,8 @@ @testable import GraphQL -import NIO import XCTest class InputTests: XCTestCase { - func testArgsNonNullNoDefault() throws { + func testArgsNonNullNoDefault() async throws { struct Echo: Codable { let field1: String } @@ -20,7 +19,7 @@ class InputTests: XCTestCase { type: GraphQLNonNull(GraphQLString) ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -48,49 +47,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -99,77 +93,73 @@ class InputTests: XCTestCase { ) // Test providing null results in an error - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait() - .errors.count > 0 + } + """ ) XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result.errors.count > 0 + ) + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait() - .errors.count > 0 + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertTrue( + result.errors.count > 0 ) // Test not providing parameter results in an error - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait() - .errors.count > 0 + } + """ ) XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result.errors.count > 0 + ) + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait() - .errors.count > 0 + } + """, + variableValues: [:] + ) + XCTAssertTrue( + result.errors.count > 0 ) } - func testArgsNullNoDefault() throws { + func testArgsNullNoDefault() async throws { struct Echo: Codable { let field1: String? } @@ -186,7 +176,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -214,49 +204,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -265,43 +250,43 @@ class InputTests: XCTestCase { ) // Test providing null is accepted - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait(), + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -310,39 +295,39 @@ class InputTests: XCTestCase { ) // Test not providing parameter is accepted - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -351,7 +336,7 @@ class InputTests: XCTestCase { ) } - func testArgsNonNullDefault() throws { + func testArgsNonNullDefault() async throws { struct Echo: Codable { let field1: String } @@ -368,7 +353,7 @@ class InputTests: XCTestCase { type: GraphQLNonNull(GraphQLString) ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -397,49 +382,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -448,76 +428,74 @@ class InputTests: XCTestCase { ) // Test providing null results in an error - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait() - .errors.count > 0 + } + """ ) XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result.errors.count > 0 + ) + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait() - .errors.count > 0 + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertTrue( + result.errors.count > 0 ) // Test not providing parameter results in default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String! = "defaultValue1") { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String! = "defaultValue1") { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", @@ -526,26 +504,25 @@ class InputTests: XCTestCase { ) // Test variable doesn't get argument default - XCTAssertTrue( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait() - .errors.count > 0 + } + """, + variableValues: [:] + ) + XCTAssertTrue( + result.errors.count > 0 ) } - func testArgsNullDefault() throws { + func testArgsNullDefault() async throws { struct Echo: Codable { let field1: String? } @@ -562,7 +539,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -591,49 +568,44 @@ class InputTests: XCTestCase { types: [EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test basic functionality - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: "value1" - ) { - field1 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: "value1" + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String!) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String!) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": "value1", - ] - ).wait(), + } + """, + variableValues: [ + "field1": "value1", + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -642,43 +614,43 @@ class InputTests: XCTestCase { ) // Test providing null results in a null output - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo( - field1: null - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo( + field1: null + ) { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [ - "field1": .null, - ] - ).wait(), + } + """, + variableValues: [ + "field1": .null, + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -687,39 +659,39 @@ class InputTests: XCTestCase { ) // Test not providing parameter results in default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo { - field1 - } + result = try await graphql( + schema: schema, + request: """ + { + echo { + field1 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", ], ]) ) - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String = "defaultValue1") { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String = "defaultValue1") { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "defaultValue1", @@ -728,21 +700,21 @@ class InputTests: XCTestCase { ) // Test that nullable unprovided variables are coerced to null - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($field1: String) { - echo ( - field1: $field1 - ) { - field1 - } + result = try await graphql( + schema: schema, + request: """ + query echo($field1: String) { + echo ( + field1: $field1 + ) { + field1 } - """, - eventLoopGroup: group, - variableValues: [:] - ).wait(), + } + """, + variableValues: [:] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": .null, @@ -752,7 +724,7 @@ class InputTests: XCTestCase { } // Test that input objects parse as expected from non-null literals - func testInputNoNull() throws { + func testInputNoNull() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -798,7 +770,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -827,28 +799,23 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test in arguments - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1", - field2: "value2", - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1", + field2: "value2", + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -858,25 +825,25 @@ class InputTests: XCTestCase { ) // Test in variables - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - "field2": "value2", - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + "field2": "value2", + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -887,7 +854,7 @@ class InputTests: XCTestCase { } // Test that inputs parse as expected when null literals are present - func testInputParsingDefinedNull() throws { + func testInputParsingDefinedNull() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -933,7 +900,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -962,28 +929,23 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test in arguments - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1", - field2: null, - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1", + field2: null, + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -993,25 +955,25 @@ class InputTests: XCTestCase { ) // Test in variables - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - "field2": .null, - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + "field2": .null, + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1022,7 +984,7 @@ class InputTests: XCTestCase { } // Test that input objects parse as expected when there are missing fields with no default - func testInputParsingUndefined() throws { + func testInputParsingUndefined() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -1073,7 +1035,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -1102,27 +1064,22 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Test in arguments - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1" - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1132,24 +1089,24 @@ class InputTests: XCTestCase { ) // Test in variables - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1160,7 +1117,7 @@ class InputTests: XCTestCase { } // Test that input objects parse as expected when there are missing fields with defaults - func testInputParsingUndefinedWithDefault() throws { + func testInputParsingUndefinedWithDefault() async throws { struct Echo: Codable { let field1: String? let field2: String? @@ -1207,7 +1164,7 @@ class InputTests: XCTestCase { type: GraphQLString ), ], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Echo } ) @@ -1236,27 +1193,22 @@ class InputTests: XCTestCase { types: [EchoInputType, EchoOutputType] ) - let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try group.syncShutdownGracefully()) - } - // Undefined with default gets default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1" - }) { - field1 - field2 - } + var result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1265,22 +1217,22 @@ class InputTests: XCTestCase { ]) ) // Null literal with default gets null - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - { - echo(input:{ - field1: "value1" - field2: null - }) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + { + echo(input:{ + field1: "value1" + field2: null + }) { + field1 + field2 } - """, - eventLoopGroup: group - ).wait(), + } + """ + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1291,24 +1243,24 @@ class InputTests: XCTestCase { // Test in variable // Undefined with default gets default - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", @@ -1317,25 +1269,25 @@ class InputTests: XCTestCase { ]) ) // Null literal with default gets null - XCTAssertEqual( - try graphql( - schema: schema, - request: """ - query echo($input: EchoInput) { - echo(input: $input) { - field1 - field2 - } + result = try await graphql( + schema: schema, + request: """ + query echo($input: EchoInput) { + echo(input: $input) { + field1 + field2 } - """, - eventLoopGroup: group, - variableValues: [ - "input": [ - "field1": "value1", - "field2": .null, - ], - ] - ).wait(), + } + """, + variableValues: [ + "input": [ + "field1": "value1", + "field2": .null, + ], + ] + ) + XCTAssertEqual( + result, GraphQLResult(data: [ "echo": [ "field1": "value1", diff --git a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift b/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift deleted file mode 100644 index 56a77b93..00000000 --- a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift +++ /dev/null @@ -1,197 +0,0 @@ -import Dispatch -import Foundation -import GraphQL -import NIO -import XCTest - -class InstrumentationTests: XCTestCase, Instrumentation { - class MyRoot {} - class MyCtx {} - - var query = "query sayHello($name: String) { hello(name: $name) }" - var expectedResult: Map = [ - "data": [ - "hello": "bob", - ], - ] - var expectedThreadId = 0 - var expectedProcessId = 0 - var expectedRoot = MyRoot() - var expectedCtx = MyCtx() - var expectedOpVars: [String: Map] = ["name": "bob"] - var expectedOpName = "sayHello" - var queryParsingCalled = 0 - var queryValidationCalled = 0 - var operationExecutionCalled = 0 - var fieldResolutionCalled = 0 - - let schema = try! GraphQLSchema( - query: GraphQLObjectType( - name: "RootQueryType", - fields: [ - "hello": GraphQLField( - type: GraphQLString, - args: [ - "name": GraphQLArgument(type: GraphQLNonNull(GraphQLString)), - ], - resolve: { inputValue, _, _, _ in - print(type(of: inputValue)) - return nil - } -// resolve: { _, args, _, _ in return try! args["name"].asString() } - ), - ] - ) - ) - - override func setUp() { - expectedThreadId = 0 - expectedProcessId = 0 - queryParsingCalled = 0 - queryValidationCalled = 0 - operationExecutionCalled = 0 - fieldResolutionCalled = 0 - } - - func queryParsing( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Source, - result _: Result - ) { -// queryParsingCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertEqual(source.name, "GraphQL request") -// switch result { -// case .error(let e): -// XCTFail("unexpected error \(e)") -// case .result(let document): -// XCTAssertEqual(document.loc!.source.name, source.name) -// } -// XCTAssertEqual(source.name, "GraphQL request") - } - - func queryValidation( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - errors _: [GraphQLError] - ) { - queryValidationCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertTrue(schema === self.schema) -// XCTAssertEqual(document.loc!.source.name, "GraphQL request") -// XCTAssertEqual(errors, []) - } - - func operationExecution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - schema _: GraphQLSchema, - document _: Document, - rootValue _: Any, - eventLoopGroup _: EventLoopGroup, - variableValues _: [String: Map], - operation _: OperationDefinition?, - errors _: [GraphQLError], - result _: Map - ) { -// operationExecutionCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertTrue(schema === self.schema) -// XCTAssertEqual(document.loc?.source.name ?? "", "GraphQL request") -// XCTAssertTrue(rootValue as! MyRoot === expectedRoot) -// XCTAssertTrue(contextValue as! MyCtx === expectedCtx) -// XCTAssertEqual(variableValues, expectedOpVars) -// XCTAssertEqual(operation!.name!.value, expectedOpName) -// XCTAssertEqual(errors, []) -// XCTAssertEqual(result, expectedResult) - } - - func fieldResolution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Any, - args _: Map, - eventLoopGroup _: EventLoopGroup, - info _: GraphQLResolveInfo, - result _: Result, Error> - ) { - fieldResolutionCalled += 1 -// XCTAssertEqual(processId, expectedProcessId, "unexpected process id") -// XCTAssertEqual(threadId, expectedThreadId, "unexpected thread id") -// XCTAssertGreaterThan(finished, started) -// XCTAssertTrue(source as! MyRoot === expectedRoot) -// XCTAssertEqual(args, try! expectedOpVars.asMap()) -// XCTAssertTrue(context as! MyCtx === expectedCtx) -// switch result { -// case .error(let e): -// XCTFail("unexpected error \(e)") -// case .result(let r): -// XCTAssertEqual(r as! String, try! expectedResult["data"]["hello"].asString()) -// } - } - - func testInstrumentationCalls() throws { -// #if os(Linux) || os(Android) -// expectedThreadId = Int(pthread_self()) -// #else -// expectedThreadId = Int(pthread_mach_thread_np(pthread_self())) -// #endif -// expectedProcessId = Int(getpid()) -// let result = try graphql( -// instrumentation: self, -// schema: schema, -// request: query, -// rootValue: expectedRoot, -// contextValue: expectedCtx, -// variableValues: expectedOpVars, -// operationName: expectedOpName -// ) -// XCTAssertEqual(result, expectedResult) -// XCTAssertEqual(queryParsingCalled, 1) -// XCTAssertEqual(queryValidationCalled, 1) -// XCTAssertEqual(operationExecutionCalled, 1) -// XCTAssertEqual(fieldResolutionCalled, 1) - } - - func testDispatchQueueInstrumentationWrapper() throws { -// let dispatchGroup = DispatchGroup() -// #if os(Linux) || os(Android) -// expectedThreadId = Int(pthread_self()) -// #else -// expectedThreadId = Int(pthread_mach_thread_np(pthread_self())) -// #endif -// expectedProcessId = Int(getpid()) -// let result = try graphql( -// instrumentation: DispatchQueueInstrumentationWrapper(self, dispatchGroup: dispatchGroup), -// schema: schema, -// request: query, -// rootValue: expectedRoot, -// contextValue: expectedCtx, -// variableValues: expectedOpVars, -// operationName: expectedOpName -// ) -// dispatchGroup.wait() -// XCTAssertEqual(result, expectedResult) -// XCTAssertEqual(queryParsingCalled, 1) -// XCTAssertEqual(queryValidationCalled, 1) -// XCTAssertEqual(operationExecutionCalled, 1) -// XCTAssertEqual(fieldResolutionCalled, 1) - } -} diff --git a/Tests/GraphQLTests/LanguageTests/ParserTests.swift b/Tests/GraphQLTests/LanguageTests/ParserTests.swift index 065c1c9f..f0f1d574 100644 --- a/Tests/GraphQLTests/LanguageTests/ParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/ParserTests.swift @@ -282,7 +282,7 @@ class ParserTests: XCTestCase { func testKitchenSink() throws { guard let url = Bundle.module.url(forResource: "kitchen-sink", withExtension: "graphql"), - let kitchenSink = try? String(contentsOf: url) + let kitchenSink = try? String(contentsOf: url, encoding: .utf8) else { XCTFail("Could not load kitchen sink") return diff --git a/Tests/GraphQLTests/LanguageTests/PrinterTests.swift b/Tests/GraphQLTests/LanguageTests/PrinterTests.swift index 086f29e4..0b46ffa5 100644 --- a/Tests/GraphQLTests/LanguageTests/PrinterTests.swift +++ b/Tests/GraphQLTests/LanguageTests/PrinterTests.swift @@ -164,7 +164,7 @@ class PrinterTests: XCTestCase { func testPrintsKitchenSinkWithoutAlteringAST() throws { guard let url = Bundle.module.url(forResource: "kitchen-sink", withExtension: "graphql"), - let kitchenSink = try? String(contentsOf: url) + let kitchenSink = try? String(contentsOf: url, encoding: .utf8) else { XCTFail("Could not load kitchen sink") return diff --git a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift index a84c4833..b76464ec 100644 --- a/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift +++ b/Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift @@ -1379,7 +1379,7 @@ class SchemaParserTests: XCTestCase { forResource: "schema-kitchen-sink", withExtension: "graphql" ), - let kitchenSink = try? String(contentsOf: url) + let kitchenSink = try? String(contentsOf: url, encoding: .utf8) else { XCTFail("Could not load kitchen sink") return diff --git a/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift b/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift index 234a3bdf..7fdcc4a4 100644 --- a/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift +++ b/Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift @@ -15,7 +15,7 @@ class SchemaPrinterTests: XCTestCase { forResource: "schema-kitchen-sink", withExtension: "graphql" ), - let kitchenSink = try? String(contentsOf: url) + let kitchenSink = try? String(contentsOf: url, encoding: .utf8) else { XCTFail("Could not load kitchen sink") return diff --git a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 2b78e00a..9ca4b045 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -87,9 +87,10 @@ class VisitorTests: XCTestCase { let newName = node.name .map { Name(loc: $0.loc, value: $0.value + ".enter") } ?? Name(value: "enter") - node.set(value: .node(newName), key: "name") - node.set(value: .node(SelectionSet(selections: [])), key: "selectionSet") - return .node(node) + let newNode = node + .set(value: .node(newName), key: "name") + .set(value: .node(SelectionSet(selections: [])), key: "selectionSet") + return .node(newNode) } return .continue }, @@ -99,9 +100,10 @@ class VisitorTests: XCTestCase { let newName = node.name .map { Name(loc: $0.loc, value: $0.value + ".leave") } ?? Name(value: "leave") - node.set(value: .node(newName), key: "name") - node.set(value: .node(selectionSet!), key: "selectionSet") - return .node(node) + let newNode = node + .set(value: .node(newName), key: "name") + .set(value: .node(selectionSet!), key: "selectionSet") + return .node(newNode) } return .continue } @@ -129,11 +131,11 @@ class VisitorTests: XCTestCase { locations: [.init(value: "root")] ) ) - node.set( + let newNode = node.set( value: .array(newDefinitions), key: "definitions" ) - return .node(node) + return .node(newNode) } return .continue }, @@ -147,11 +149,11 @@ class VisitorTests: XCTestCase { locations: [.init(value: "root")] ) ) - node.set( + let newNode = node.set( value: .array(newDefinitions), key: "definitions" ) - return .node(node) + return .node(newNode) } return .continue } @@ -266,11 +268,13 @@ class VisitorTests: XCTestCase { var newSelections = selectionSet.selections newSelections.append(addedField) - let newSelectionSet = selectionSet - newSelectionSet.set(value: .array(newSelections), key: "selections") + var newSelectionSet = selectionSet + newSelectionSet = newSelectionSet.set( + value: .array(newSelections), + key: "selections" + ) - let newNode = node - newNode.set(value: .node(newSelectionSet), key: "selectionSet") + let newNode = node.set(value: .node(newSelectionSet), key: "selectionSet") return .node(newNode) } } @@ -500,7 +504,7 @@ class VisitorTests: XCTestCase { guard let url = Bundle.module.url(forResource: "kitchen-sink", withExtension: "graphql"), - let kitchenSink = try? String(contentsOf: url) + let kitchenSink = try? String(contentsOf: url, encoding: .utf8) else { XCTFail("Could not load kitchen sink") return diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsData.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsData.swift index dce0c18d..c7959727 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsData.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsData.swift @@ -8,7 +8,7 @@ import GraphQL * values in a more complex demo. */ -enum Episode: String, Encodable { +enum Episode: String, Encodable, Sendable { case newHope = "NEWHOPE" case empire = "EMPIRE" case jedi = "JEDI" @@ -22,7 +22,7 @@ enum Episode: String, Encodable { } } -protocol Character: Encodable { +protocol Character: Encodable, Sendable { var id: String { get } var name: String { get } var friends: [String] { get } @@ -129,14 +129,14 @@ let droidData: [String: Droid] = [ /** * Helper function to get a character by ID. */ -func getCharacter(id: String) -> Character? { +@Sendable func getCharacter(id: String) -> Character? { return humanData[id] ?? droidData[id] } /** * Allows us to query for a character"s friends. */ -func getFriends(character: Character) -> [Character] { +@Sendable func getFriends(character: Character) -> [Character] { return character.friends.reduce(into: []) { friends, friendID in if let friend = getCharacter(id: friendID) { friends.append(friend) @@ -147,7 +147,7 @@ func getFriends(character: Character) -> [Character] { /** * Allows us to fetch the undisputed hero of the Star Wars trilogy, R2-D2. */ -func getHero(episode: Episode?) -> Character { +@Sendable func getHero(episode: Episode?) -> Character { if episode == .empire { // Luke is the hero of Episode V. return luke @@ -159,13 +159,13 @@ func getHero(episode: Episode?) -> Character { /** * Allows us to query for the human with the given id. */ -func getHuman(id: String) -> Human? { +@Sendable func getHuman(id: String) -> Human? { return humanData[id] } /** * Allows us to query for the droid with the given id. */ -func getDroid(id: String) -> Droid? { +@Sendable func getDroid(id: String) -> Droid? { return droidData[id] } diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift index ff8b4b5b..3ce7a76e 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsIntrospectionTests.swift @@ -1,15 +1,9 @@ -import NIO import XCTest @testable import GraphQL class StarWarsIntrospectionTests: XCTestCase { - func testIntrospectionTypeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionTypeQuery() async throws { do { let query = "query IntrospectionTypeQuery {" + " __schema {" + @@ -73,23 +67,17 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } catch { print(error) } } - func testIntrospectionQueryTypeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionQueryTypeQuery() async throws { let query = "query IntrospectionQueryTypeQuery {" + " __schema {" + " queryType {" + @@ -108,20 +96,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidTypeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidTypeQuery() async throws { let query = "query IntrospectionDroidTypeQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -136,20 +118,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidKindQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidKindQuery() async throws { let query = "query IntrospectionDroidKindQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -166,20 +142,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionCharacterKindQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionCharacterKindQuery() async throws { let query = "query IntrospectionCharacterKindQuery {" + " __type(name: \"Character\") {" + " name" + @@ -196,20 +166,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidFieldsQuery() async throws { let query = "query IntrospectionDroidFieldsQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -275,20 +239,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidNestedFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidNestedFieldsQuery() async throws { let query = "query IntrospectionDroidNestedFieldsQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -373,20 +331,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionFieldArgsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionFieldArgsQuery() async throws { let query = "query IntrospectionFieldArgsQuery {" + " __schema {" + " queryType {" + @@ -472,20 +424,14 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testIntrospectionDroidDescriptionQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testIntrospectionDroidDescriptionQuery() async throws { let query = "query IntrospectionDroidDescriptionQuery {" + " __type(name: \"Droid\") {" + " name" + @@ -502,11 +448,10 @@ class StarWarsIntrospectionTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } } diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift index 368436ad..d0053524 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsQueryTests.swift @@ -1,16 +1,9 @@ -import NIO import XCTest @testable import GraphQL class StarWarsQueryTests: XCTestCase { - func testHeroNameQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testHeroNameQuery() async throws { let query = """ query HeroNameQuery { hero { @@ -27,22 +20,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testHeroNameAndFriendsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testHeroNameAndFriendsQuery() async throws { let query = """ query HeroNameAndFriendsQuery { hero { @@ -69,22 +55,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testNestedQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testNestedQuery() async throws { let query = """ query NestedQuery { hero { @@ -139,20 +118,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testFetchLukeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchLukeQuery() async throws { let query = """ query FetchLukeQuery { @@ -170,20 +143,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testOptionalVariable() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testOptionalVariable() async throws { let query = """ query FetchHeroByEpisodeQuery($episode: Episode) { @@ -208,12 +175,11 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) // or we can pass "EMPIRE" and expect Luke @@ -229,21 +195,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) } - func testFetchSomeIDQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchSomeIDQuery() async throws { let query = """ query FetchSomeIDQuery($someId: String!) { @@ -269,12 +229,11 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) params = [ @@ -289,12 +248,11 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) params = [ @@ -307,21 +265,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - result = try graphql( + result = try await graphql( schema: starWarsSchema, request: query, - eventLoopGroup: eventLoopGroup, variableValues: params - ).wait() + ) XCTAssertEqual(result, expected) } - func testFetchLukeAliasedQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchLukeAliasedQuery() async throws { let query = """ query FetchLukeAliasedQuery { @@ -339,20 +291,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testFetchLukeAndLeiaAliasedQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testFetchLukeAndLeiaAliasedQuery() async throws { let query = """ query FetchLukeAndLeiaAliasedQuery { @@ -376,20 +322,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testDuplicateFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testDuplicateFieldsQuery() async throws { let query = """ query DuplicateFieldsQuery { @@ -417,20 +357,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testUseFragmentQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testUseFragmentQuery() async throws { let query = """ query UseFragmentQuery { @@ -460,20 +394,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testCheckTypeOfR2Query() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testCheckTypeOfR2Query() async throws { let query = """ query CheckTypeOfR2Query { @@ -493,21 +421,15 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testCheckTypeOfLukeQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testCheckTypeOfLukeQuery() async throws { let query = """ query CheckTypeOfLukeQuery { @@ -527,20 +449,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testSecretBackstoryQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testSecretBackstoryQuery() async throws { let query = """ query SecretBackstoryQuery { @@ -567,20 +483,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testSecretBackstoryListQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testSecretBackstoryListQuery() async throws { let query = """ query SecretBackstoryListQuery { @@ -633,20 +543,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testSecretBackstoryAliasQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testSecretBackstoryAliasQuery() async throws { let query = """ query SecretBackstoryAliasQuery { @@ -673,20 +577,14 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql( + let result = try await graphql( schema: starWarsSchema, - request: query, - eventLoopGroup: eventLoopGroup - ).wait() + request: query + ) XCTAssertEqual(result, expected) } - func testNonNullableFieldsQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - + func testNonNullableFieldsQuery() async throws { let A = try GraphQLObjectType( name: "A", fields: [:] @@ -762,19 +660,13 @@ class StarWarsQueryTests: XCTestCase { ] ) - let result = try graphql(schema: schema, request: query, eventLoopGroup: eventLoopGroup) - .wait() + let result = try await graphql(schema: schema, request: query) + XCTAssertEqual(result, expected) } - func testFieldOrderQuery() throws { - let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - - defer { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - - XCTAssertEqual(try graphql( + func testFieldOrderQuery() async throws { + var result = try await graphql( schema: starWarsSchema, request: """ query HeroNameQuery { @@ -783,9 +675,9 @@ class StarWarsQueryTests: XCTestCase { name } } - """, - eventLoopGroup: eventLoopGroup - ).wait(), GraphQLResult( + """ + ) + XCTAssertEqual(result, GraphQLResult( data: [ "hero": [ "id": "2001", @@ -794,7 +686,7 @@ class StarWarsQueryTests: XCTestCase { ] )) - XCTAssertNotEqual(try graphql( + result = try await graphql( schema: starWarsSchema, request: """ query HeroNameQuery { @@ -803,9 +695,9 @@ class StarWarsQueryTests: XCTestCase { name } } - """, - eventLoopGroup: eventLoopGroup - ).wait(), GraphQLResult( + """ + ) + XCTAssertNotEqual(result, GraphQLResult( data: [ "hero": [ "name": "R2-D2", diff --git a/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift b/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift index d9d70562..23841438 100644 --- a/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift +++ b/Tests/GraphQLTests/StarWarsTests/StarWarsSchema.swift @@ -111,7 +111,7 @@ let CharacterInterface = try! GraphQLInterfaceType( description: "All secrets about their past." ), ] }, - resolveType: { character, _, _ in + resolveType: { character, _ in switch character { case is Human: return "Human" @@ -174,7 +174,7 @@ let HumanType = try! GraphQLObjectType( ), ], interfaces: [CharacterInterface], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Human } ) @@ -232,7 +232,7 @@ let DroidType = try! GraphQLObjectType( ), ], interfaces: [CharacterInterface], - isTypeOf: { source, _, _ in + isTypeOf: { source, _ in source is Droid } ) diff --git a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index 8a742378..906e8c1e 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -1,8 +1,7 @@ import GraphQL /// A very simple publish/subscriber used for testing -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -class SimplePubSub { +actor SimplePubSub { private var subscribers: [Subscriber] init() { @@ -21,8 +20,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) @@ -33,7 +32,6 @@ class SimplePubSub { ) subscribers.append(subscriber) } - return ConcurrentEventStream(asyncStream) } } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index fb940c72..beaf8600 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO // MARK: Types @@ -19,11 +18,11 @@ struct Email: Encodable { } } -struct Inbox: Encodable { +struct Inbox: Encodable, Sendable { let emails: [Email] } -struct EmailEvent: Encodable { +struct EmailEvent: Encodable, Sendable { let email: Email let inbox: Inbox } @@ -89,12 +88,9 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) -class EmailDb { +actor EmailDb { var emails: [Email] - let publisher: SimplePubSub + let publisher: SimplePubSub init() { emails = [ @@ -105,44 +101,38 @@ class EmailDb { unread: false ), ] - publisher = SimplePubSub() + publisher = SimplePubSub() } /// Adds a new email to the database and triggers all observers - func trigger(email: Email) { + func trigger(email: Email) async { emails.append(email) - publisher.emit(event: email) + await publisher.emit(event: email) } - func stop() { - publisher.cancel() + func stop() async { + await publisher.cancel() } /// Returns the default email schema, with standard resolvers. func defaultSchema() throws -> GraphQLSchema { return try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + resolve: { emailAny, _, _, _ throws -> Any? in if let email = emailAny as? Email { - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return await EmailEvent( email: email, inbox: Inbox(emails: self.emails) - )) + ) } else { throw GraphQLError(message: "\(type(of: emailAny)) is not Email") } }, - subscribe: { _, args, _, eventLoopGroup, _ throws -> EventLoopFuture in + subscribe: { _, args, _, _ throws -> Any? 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)) + let filtered = await self.publisher.subscribe().filter { email throws in + return email.priority >= priority + } + return filtered } ) } @@ -151,8 +141,8 @@ class EmailDb { func subscription( query: String, variableValues: [String: Map] = [:] - ) throws -> SubscriptionEventStream { - return try createSubscription( + ) async throws -> AsyncThrowingStream { + return try await createSubscription( schema: defaultSchema(), query: query, variableValues: variableValues @@ -191,24 +181,14 @@ func createSubscription( schema: GraphQLSchema, query: String, variableValues: [String: Map] = [:] -) throws -> SubscriptionEventStream { - let result = try graphqlSubscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, +) async throws -> AsyncThrowingStream { + let result = try await graphqlSubscribe( 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 result.get() } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index db0ebfb6..5f9a8da9 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -1,9 +1,7 @@ import GraphQL -import NIO import XCTest /// This follows the graphql-js testing, with deviations where noted. -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class SubscriptionTests: XCTestCase { let timeoutDuration = 0.5 // in seconds @@ -12,7 +10,7 @@ class SubscriptionTests: XCTestCase { /// 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 schema = try await db.defaultSchema() let query = """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { @@ -30,27 +28,19 @@ class SubscriptionTests: XCTestCase { let subscriptionResult = try await graphqlSubscribe( schema: schema, - request: query, - eventLoopGroup: eventLoopGroup - ).get() - 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() + request: query + ) + let stream = try subscriptionResult.get() + var iterator = stream.makeAsyncIterator() - db.trigger(email: Email( + await 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() + await db.stop() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -85,23 +75,19 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + resolve: { emailAny, _, _, _ throws in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" ) } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, - inbox: Inbox(emails: db.emails) - )) + inbox: Inbox(emails: await db.emails) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws in + await db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( @@ -111,29 +97,25 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + resolve: { emailAny, _, _, _ throws in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" ) } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, - inbox: Inbox(emails: db.emails) - )) + inbox: Inbox(emails: await db.emails) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws in + await db.publisher.subscribe() } ), ] ) ) - let subscription = try createSubscription(schema: schema, query: """ + let stream = try await createSubscription(schema: schema, query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -147,20 +129,16 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -185,8 +163,18 @@ class SubscriptionTests: XCTestCase { func testInvalidMultiField() async throws { let db = EmailDb() - var didResolveImportantEmail = false - var didResolveNonImportantEmail = false + actor ResolveChecker { + var didResolveImportantEmail = false + var didResolveNonImportantEmail = false + func resolveImportantEmail() async { + didResolveImportantEmail = true + } + + func resolveNonImportantEmail() async { + didResolveNonImportantEmail = true + } + } + let resolveChecker = ResolveChecker() let schema = try GraphQLSchema( query: EmailQueryType, @@ -195,34 +183,28 @@ class SubscriptionTests: XCTestCase { fields: [ "importantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - didResolveImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ in + await resolveChecker.resolveImportantEmail() + return await db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - didResolveNonImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ in + await resolveChecker.resolveNonImportantEmail() + return await db.publisher.subscribe() } ), ] ) ) - let _ = try createSubscription(schema: schema, query: """ + let subscriptionResult = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -236,15 +218,20 @@ class SubscriptionTests: XCTestCase { } } """) + var iterator = subscriptionResult.makeAsyncIterator() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) + _ = try await iterator.next() + // One and only one should be true + let didResolveImportantEmail = await resolveChecker.didResolveImportantEmail + let didResolveNonImportantEmail = await resolveChecker.didResolveNonImportantEmail XCTAssertTrue(didResolveImportantEmail || didResolveNonImportantEmail) XCTAssertFalse(didResolveImportantEmail && didResolveNonImportantEmail) } @@ -256,47 +243,53 @@ class SubscriptionTests: XCTestCase { // Not implemented because this is taken care of by Swift optional types /// 'resolves to an error for unknown subscription field' - func testErrorUnknownSubscriptionField() throws { + func testErrorUnknownSubscriptionField() async throws { let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: """ + do { + _ = try await db.subscription(query: """ subscription { unknownField } """) - ) { error in - guard let graphQLError = error as? GraphQLError else { - XCTFail("Error was not of type GraphQLError") + XCTFail("Error should have been thrown") + } catch { + guard let graphQLErrors = error as? GraphQLErrors else { + XCTFail("Error was not of type GraphQLErrors") return } XCTAssertEqual( - graphQLError.message, - "Cannot query field \"unknownField\" on type \"Subscription\"." + graphQLErrors.errors, + [ + GraphQLError( + message: "Cannot query field \"unknownField\" on type \"Subscription\".", + locations: [SourceLocation(line: 2, column: 5)] + ), + ] ) - XCTAssertEqual(graphQLError.locations, [SourceLocation(line: 2, column: 5)]) } } /// 'should pass through unexpected errors thrown in subscribe' - func testPassUnexpectedSubscribeErrors() throws { + func testPassUnexpectedSubscribeErrors() async throws { let db = EmailDb() - XCTAssertThrowsError( - try db.subscription(query: "") - ) + do { + _ = try await db.subscription(query: "") + XCTFail("Error should have been thrown") + } catch {} } /// 'throws an error if subscribe does not return an iterator' - func testErrorIfSubscribeIsntIterator() throws { + func testErrorIfSubscribeIsntIterator() async throws { let schema = try emailSchemaWithResolvers( - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ throws in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture("test") + subscribe: { _, _, _, _ throws in + "test" } ) - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ + do { + _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -305,23 +298,26 @@ class SubscriptionTests: XCTestCase { } } """) - ) { error in - guard let graphQLError = error as? GraphQLError else { + XCTFail("Error should have been thrown") + } catch { + guard let graphQLErrors = error as? GraphQLErrors else { XCTFail("Error was not of type GraphQLError") return } XCTAssertEqual( - graphQLError.message, - "Subscription field resolver must return EventStream. Received: 'test'" + graphQLErrors.errors, + [GraphQLError( + message: "Subscription field resolver must return an AsyncSequence. Received: 'test'" + )] ) } } /// 'resolves to an error for subscription resolver errors' - func testErrorForSubscriptionResolverErrors() throws { - func verifyError(schema: GraphQLSchema) { - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ + func testErrorForSubscriptionResolverErrors() async throws { + func verifyError(schema: GraphQLSchema) async throws { + do { + _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -330,33 +326,34 @@ class SubscriptionTests: XCTestCase { } } """) - ) { error in - guard let graphQLError = error as? GraphQLError else { + XCTFail("Error should have been thrown") + } catch { + guard let graphQLErrors = error as? GraphQLErrors else { XCTFail("Error was not of type GraphQLError") return } - XCTAssertEqual(graphQLError.message, "test error") + XCTAssertEqual(graphQLErrors.errors, [GraphQLError(message: "test error")]) } } // Throwing an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _, _ throws -> EventLoopFuture in + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws 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")) + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws in + GraphQLError(message: "test error") } )) // Rejecting with an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeFailedFuture(GraphQLError(message: "test error")) + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws in + GraphQLError(message: "test error") } )) } @@ -365,7 +362,7 @@ class SubscriptionTests: XCTestCase { // Tests above cover this /// 'resolves to an error if variables were wrong type' - func testErrorVariablesWrongType() throws { + func testErrorVariablesWrongType() async throws { let db = EmailDb() let query = """ subscription ($priority: Int) { @@ -382,14 +379,15 @@ class SubscriptionTests: XCTestCase { } """ - XCTAssertThrowsError( - try db.subscription( + do { + _ = try await db.subscription( query: query, variableValues: [ "priority": "meow", ] ) - ) { error in + XCTFail("Should have thrown error") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -406,7 +404,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for a single subscriber' func testSingleSubscriber() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -420,21 +418,17 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) - db.stop() + await db.stop() - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -455,7 +449,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for multiple subscribe in same subscription' func testMultipleSubscribers() async throws { let db = EmailDb() - let subscription1 = try db.subscription(query: """ + let stream1 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -469,12 +463,8 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream1 = subscription1 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - let subscription2 = try db.subscription(query: """ + let stream2 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -488,23 +478,19 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream2 = subscription2 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator1 = stream1.stream.makeAsyncIterator() - var iterator2 = stream2.stream.makeAsyncIterator() + var iterator1 = stream1.makeAsyncIterator() + var iterator2 = stream2.makeAsyncIterator() - db.trigger(email: Email( + await 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 result1 = try await iterator1.next() + let result2 = try await iterator2.next() let expected = GraphQLResult( data: ["importantEmail": [ @@ -526,7 +512,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload per subscription event' func testPayloadPerEvent() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -540,20 +526,16 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream = subscription as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - var iterator = stream.stream.makeAsyncIterator() + var iterator = stream.makeAsyncIterator() // A new email arrives! - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) - let result1 = try await iterator.next()?.get() + let result1 = try await iterator.next() XCTAssertEqual( result1, GraphQLResult( @@ -571,13 +553,13 @@ class SubscriptionTests: XCTestCase { ) // Another new email arrives - db.trigger(email: Email( + await db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", unread: true )) - let result2 = try await iterator.next()?.get() + let result2 = try await iterator.next() XCTAssertEqual( result2, GraphQLResult( @@ -599,7 +581,7 @@ class SubscriptionTests: XCTestCase { /// This is not in the graphql-js tests. func testArguments() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 5) { importantEmail(priority: $priority) { email { @@ -613,26 +595,11 @@ class SubscriptionTests: XCTestCase { } } """) - 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 iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -653,25 +620,21 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) // Low priority email shouldn't trigger an event - expectation = XCTestExpectation() - expectation.isInverted = true - db.trigger(email: Email( + await 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( + await db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", @@ -692,18 +655,14 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - - // 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: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -717,24 +676,11 @@ class SubscriptionTests: XCTestCase { } } """) - 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 iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -754,28 +700,20 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - db.stop() + await db.stop() // This should not trigger an event. - expectation = XCTestExpectation() - expectation.isInverted = true - db.trigger(email: Email( + await 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 } /// 'should not trigger when subscription is thrown' @@ -784,40 +722,34 @@ class SubscriptionTests: XCTestCase { /// 'event order is correct for multiple publishes' func testOrderCorrectForMultiplePublishes() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let stream = try await 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() + var iterator = stream.makeAsyncIterator() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", unread: true )) - db.trigger(email: Email( + await 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() + let result1 = try await iterator.next() + let result2 = try await iterator.next() + XCTAssertEqual( result1, GraphQLResult( @@ -826,15 +758,9 @@ class SubscriptionTests: XCTestCase { "from": "yuzhi@graphql.org", "subject": "Alright", ], - "inbox": [ - "unread": 2, - "total": 3, - ], ]] ) ) - - let result2 = try await iterator.next()?.get() XCTAssertEqual( result2, GraphQLResult( @@ -843,10 +769,6 @@ class SubscriptionTests: XCTestCase { "from": "yuzhi@graphql.org", "subject": "Message 2", ], - "inbox": [ - "unread": 2, - "total": 3, - ], ]] ) ) @@ -857,7 +779,7 @@ class SubscriptionTests: XCTestCase { let db = EmailDb() let schema = try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + resolve: { emailAny, _, _, _ throws in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" @@ -866,17 +788,17 @@ class SubscriptionTests: XCTestCase { if email.subject == "Goodbye" { // Force the system to fail here. throw GraphQLError(message: "Never leave.") } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, - inbox: Inbox(emails: db.emails) - )) + inbox: Inbox(emails: await db.emails) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws in + await db.publisher.subscribe() } ) - let subscription = try createSubscription(schema: schema, query: """ + let stream = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -885,24 +807,11 @@ class SubscriptionTests: XCTestCase { } } """) - 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 iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Hello", message: "Tests are good", @@ -917,12 +826,11 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - expectation = XCTestExpectation() // An error in execution is presented as such. - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Goodbye", message: "Tests are good", @@ -936,13 +844,12 @@ class SubscriptionTests: XCTestCase { ] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - expectation = XCTestExpectation() // However that does not close the response event stream. Subsequent events are still // executed. - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Bonjour", message: "Tests are good", @@ -957,51 +864,13 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow } /// '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 - } - 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"), - ] - ) - ) - } + // Handled by strongly typed PubSub } diff --git a/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift b/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift index 915331bd..e6ed44fb 100644 --- a/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift +++ b/Tests/GraphQLTests/TypeTests/GraphQLSchemaTests.swift @@ -47,7 +47,7 @@ class GraphQLSchemaTests: XCTestCase { ), ], interfaces: [interface], - isTypeOf: { _, _, _ -> Bool in + isTypeOf: { _, _ -> Bool in preconditionFailure("Should not be called") } ) @@ -81,7 +81,7 @@ class GraphQLSchemaTests: XCTestCase { ), ], interfaces: [interface], - isTypeOf: { _, _, _ -> Bool in + isTypeOf: { _, _ -> Bool in preconditionFailure("Should not be called") } ) @@ -110,7 +110,7 @@ class GraphQLSchemaTests: XCTestCase { ), ], interfaces: [interface], - isTypeOf: { _, _, _ -> Bool in + isTypeOf: { _, _ -> Bool in preconditionFailure("Should not be called") } ) diff --git a/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift b/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift index 0a990391..c85c5bf8 100644 --- a/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift +++ b/Tests/GraphQLTests/TypeTests/IntrospectionTests.swift @@ -1,19 +1,8 @@ @testable import GraphQL -import NIO import XCTest class IntrospectionTests: XCTestCase { - private var eventLoopGroup: EventLoopGroup! - - override func setUp() { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - override func tearDown() { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - - func testDefaultValues() throws { + func testDefaultValues() async throws { let numEnum = try GraphQLEnumType( name: "Enum", values: [ @@ -110,7 +99,7 @@ class IntrospectionTests: XCTestCase { let schema = try GraphQLSchema(query: query, types: [inputObject, outputObject]) - let introspection = try graphql( + let introspection = try await graphql( schema: schema, request: """ query IntrospectionTypeQuery { @@ -133,9 +122,8 @@ class IntrospectionTests: XCTestCase { } } } - """, - eventLoopGroup: eventLoopGroup - ).wait() + """ + ) let queryType = try XCTUnwrap( introspection.data?["__schema"]["types"].array? diff --git a/Tests/GraphQLTests/TypeTests/ScalarTests.swift b/Tests/GraphQLTests/TypeTests/ScalarTests.swift index 59bc6e87..494c6b93 100644 --- a/Tests/GraphQLTests/TypeTests/ScalarTests.swift +++ b/Tests/GraphQLTests/TypeTests/ScalarTests.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO import XCTest class ScalarTests: XCTestCase { diff --git a/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift b/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift index b7a34a44..e827cf0f 100644 --- a/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift +++ b/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO import XCTest class BuildASTSchemaTests: XCTestCase { @@ -12,7 +11,7 @@ class BuildASTSchemaTests: XCTestCase { return try printSchema(schema: buildSchema(source: sdl)) } - func testCanUseBuiltSchemaForLimitedExecution() throws { + func testCanUseBuiltSchemaForLimitedExecution() async throws { let schema = try buildASTSchema( documentAST: parse( source: """ @@ -23,12 +22,11 @@ class BuildASTSchemaTests: XCTestCase { ) ) - let result = try graphql( + let result = try await graphql( schema: schema, request: "{ str }", - rootValue: ["str": 123], - eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1) - ).wait() + rootValue: ["str": 123] + ) XCTAssertEqual( result, @@ -50,16 +48,15 @@ class BuildASTSchemaTests: XCTestCase { // ) // ) // -// let result = try graphql( +// let result = try await graphql( // schema: schema, // request: "{ add(x: 34, y: 55) }", // rootValue: [ // "add": { (x: Int, y: Int) in // return x + y // } -// ], -// eventLoopGroup: MultiThreadedEventLoopGroup(numberOfThreads: 1) -// ).wait() +// ] +// ) // // XCTAssertEqual( // result, @@ -991,12 +988,12 @@ class BuildASTSchemaTests: XCTestCase { let query = try XCTUnwrap(schema.getType(name: "Query") as? GraphQLObjectType) let testInput = try XCTUnwrap(schema.getType(name: "TestInput") as? GraphQLInputObjectType) let testEnum = try XCTUnwrap(schema.getType(name: "TestEnum") as? GraphQLEnumType) - let _ = try XCTUnwrap(schema.getType(name: "TestUnion") as? GraphQLUnionType) + XCTAssertNotNil(schema.getType(name: "TestUnion") as? GraphQLUnionType) let testInterface = try XCTUnwrap( schema.getType(name: "TestInterface") as? GraphQLInterfaceType ) let testType = try XCTUnwrap(schema.getType(name: "TestType") as? GraphQLObjectType) - let _ = try XCTUnwrap(schema.getType(name: "TestScalar") as? GraphQLScalarType) + XCTAssertNotNil(schema.getType(name: "TestScalar") as? GraphQLScalarType) let testDirective = try XCTUnwrap(schema.getDirective(name: "test")) // No `Equatable` conformance diff --git a/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift b/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift index 6c3a9625..fae4c70c 100644 --- a/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift +++ b/Tests/GraphQLTests/UtilitiesTests/ExtendSchemaTests.swift @@ -1,18 +1,7 @@ @testable import GraphQL -import NIO import XCTest class ExtendSchemaTests: XCTestCase { - private var eventLoopGroup: EventLoopGroup! - - override func setUp() { - eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount) - } - - override func tearDown() { - XCTAssertNoThrow(try eventLoopGroup.syncShutdownGracefully()) - } - func schemaChanges( _ schema: GraphQLSchema, _ extendedSchema: GraphQLSchema @@ -46,7 +35,7 @@ class ExtendSchemaTests: XCTestCase { ) } - func testCanBeUsedForLimitedExecution() throws { + func testCanBeUsedForLimitedExecution() async throws { let schema = try buildSchema(source: "type Query") let extendAST = try parse(source: """ extend type Query { @@ -54,12 +43,11 @@ class ExtendSchemaTests: XCTestCase { } """) let extendedSchema = try extendSchema(schema: schema, documentAST: extendAST) - let result = try graphql( + let result = try await graphql( schema: extendedSchema, request: "{ newField }", - rootValue: ["newField": 123], - eventLoopGroup: eventLoopGroup - ).wait() + rootValue: ["newField": 123] + ) XCTAssertEqual( result, .init(data: ["newField": "123"]) diff --git a/Tests/GraphQLTests/UtilitiesTests/PrintSchemaTests.swift b/Tests/GraphQLTests/UtilitiesTests/PrintSchemaTests.swift index 5b13769c..d70e4523 100644 --- a/Tests/GraphQLTests/UtilitiesTests/PrintSchemaTests.swift +++ b/Tests/GraphQLTests/UtilitiesTests/PrintSchemaTests.swift @@ -970,7 +970,9 @@ class TypeSystemPrinterTests: XCTestCase { name: "Virus", fields: [ "name": GraphQLField(type: GraphQLNonNull(GraphQLString)), - "knownMutations": GraphQLField(type: GraphQLNonNull(GraphQLList(GraphQLNonNull(Mutation)))), + "knownMutations": GraphQLField( + type: GraphQLNonNull(GraphQLList(GraphQLNonNull(Mutation))) + ), ] ) diff --git a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift index 2350c285..f874014b 100644 --- a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift +++ b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift @@ -15,7 +15,7 @@ let ValidationExampleBeing = try! GraphQLInterfaceType( } ), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -32,7 +32,7 @@ let ValidationExampleMammal = try! GraphQLInterfaceType( "father": GraphQLField(type: ValidationExampleMammal), ] }, - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -53,7 +53,7 @@ let ValidationExamplePet = try! GraphQLInterfaceType( } ), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -78,7 +78,7 @@ let ValidationExampleCanine = try! GraphQLInterfaceType( type: ValidationExampleMammal ), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -263,7 +263,7 @@ let ValidationExampleCat = try! GraphQLObjectType( // union CatOrDog = Cat | Dog let ValidationExampleCatOrDog = try! GraphQLUnionType( name: "CatOrDog", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleCat, ValidationExampleDog] @@ -277,7 +277,7 @@ let ValidationExampleIntelligent = try! GraphQLInterfaceType( fields: [ "iq": GraphQLField(type: GraphQLInt), ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -288,12 +288,14 @@ let ValidationExampleIntelligent = try! GraphQLInterfaceType( let ValidationExampleSentient = try! GraphQLInterfaceType( name: "Sentient", fields: [ - "name": GraphQLField(type: GraphQLNonNull(GraphQLString)) { inputValue, _, _, _ -> String? in + "name": GraphQLField( + type: GraphQLNonNull(GraphQLString) + ) { inputValue, _, _, _ -> String? in print(type(of: inputValue)) return nil }, ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -305,7 +307,9 @@ let ValidationExampleSentient = try! GraphQLInterfaceType( let ValidationExampleAlien = try! GraphQLObjectType( name: "Alien", fields: [ - "name": GraphQLField(type: GraphQLNonNull(GraphQLString)) { inputValue, _, _, _ -> String? in + "name": GraphQLField( + type: GraphQLNonNull(GraphQLString) + ) { inputValue, _, _, _ -> String? in print(type(of: inputValue)) return nil }, @@ -363,7 +367,7 @@ let ValidationExampleCatCommand = try! GraphQLEnumType( // union DogOrHuman = Dog | Human let ValidationExampleDogOrHuman = try! GraphQLUnionType( name: "DogOrHuman", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleDog, ValidationExampleHuman] @@ -372,7 +376,7 @@ let ValidationExampleDogOrHuman = try! GraphQLUnionType( // union HumanOrAlien = Human | Alien let ValidationExampleHumanOrAlien = try! GraphQLUnionType( name: "HumanOrAlien", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleHuman, ValidationExampleAlien] @@ -501,7 +505,9 @@ let ValidationExampleComplicatedArgs = try! GraphQLObjectType( "stringListNonNullArgField": GraphQLField( type: GraphQLString, args: [ - "stringListNonNullArg": GraphQLArgument(type: GraphQLList(GraphQLNonNull(GraphQLString))), + "stringListNonNullArg": GraphQLArgument( + type: GraphQLList(GraphQLNonNull(GraphQLString)) + ), ], resolve: { inputValue, _, _, _ -> String? in print(type(of: inputValue)) diff --git a/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift b/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift index 36736bdf..a83759a1 100644 --- a/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift +++ b/Tests/GraphQLTests/ValidationTests/NoDeprecatedCustomRuleTests.swift @@ -132,8 +132,10 @@ class NoDeprecatedCustomRuleTests: ValidationTestCase { ], args: [ "normalArg": .init(type: GraphQLString), - "deprecatedArg": .init(type: GraphQLString, - deprecationReason: "Some arg reason."), + "deprecatedArg": .init( + type: GraphQLString, + deprecationReason: "Some arg reason." + ), ] ), ] @@ -184,8 +186,10 @@ class NoDeprecatedCustomRuleTests: ValidationTestCase { let deprecatedInputFieldSchema: GraphQLSchema = { let inputType = try! GraphQLInputObjectType(name: "InputType", fields: [ "normalField": .init(type: GraphQLString), - "deprecatedField": .init(type: GraphQLString, - deprecationReason: "Some input field reason."), + "deprecatedField": .init( + type: GraphQLString, + deprecationReason: "Some input field reason." + ), ]) return try! GraphQLSchema( query: .init(name: "Query", fields: [ @@ -273,8 +277,10 @@ class NoDeprecatedCustomRuleTests: ValidationTestCase { let deprecatedEnumValueSchema: GraphQLSchema = { let enumType = try! GraphQLEnumType(name: "EnumType", values: [ "NORMAL_VALUE": .init(value: .string("NORMAL_VALUE")), - "DEPRECATED_VALUE": .init(value: .string("DEPRECATED_VALUE"), - deprecationReason: "Some enum reason."), + "DEPRECATED_VALUE": .init( + value: .string("DEPRECATED_VALUE"), + deprecationReason: "Some enum reason." + ), ]) return try! GraphQLSchema( query: .init(name: "Query", fields: [ diff --git a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift index 91f26c6f..4e600898 100644 --- a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift +++ b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift @@ -2,7 +2,7 @@ import XCTest class ValidationTestCase: XCTestCase { - typealias Rule = (ValidationContext) -> Visitor + typealias Rule = @Sendable (ValidationContext) -> Visitor var rule: Rule! @@ -20,7 +20,7 @@ class ValidationTestCase: XCTestCase { func assertValid( _ query: String, schema: GraphQLSchema = ValidationExampleSchema, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws { let errors = try validate(body: query, schema: schema) @@ -37,7 +37,7 @@ class ValidationTestCase: XCTestCase { errorCount: Int, query: String, schema: GraphQLSchema = ValidationExampleSchema, - file: StaticString = #file, + file: StaticString = #filePath, line: UInt = #line ) throws -> [GraphQLError] { let errors = try validate(body: query, schema: schema) @@ -57,7 +57,7 @@ class ValidationTestCase: XCTestCase { column: Int, path: String = "", message: String, - testFile: StaticString = #file, + testFile: StaticString = #filePath, testLine: UInt = #line ) throws { guard let error = error else { @@ -94,7 +94,7 @@ class ValidationTestCase: XCTestCase { locations: [(line: Int, column: Int)], path: String = "", message: String, - testFile: StaticString = #file, + testFile: StaticString = #filePath, testLine: UInt = #line ) throws { guard let error = error else { @@ -131,7 +131,7 @@ class ValidationTestCase: XCTestCase { } class SDLValidationTestCase: XCTestCase { - typealias Rule = (SDLValidationContext) -> Visitor + typealias Rule = @Sendable (SDLValidationContext) -> Visitor var rule: Rule! @@ -139,7 +139,7 @@ class SDLValidationTestCase: XCTestCase { _ sdlStr: String, schema: GraphQLSchema? = nil, _ errors: [GraphQLError], - testFile _: StaticString = #file, + testFile _: StaticString = #filePath, testLine _: UInt = #line ) throws { let doc = try parse(source: sdlStr)