From 24080b6fdb5885afd7c4a1f30e2e0b6458d72dbe Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 23 Jun 2025 16:54:49 -0600 Subject: [PATCH 01/20] feat!: Uses swift concurrency under the hood This removes the NIO dependency. It is breaking because it removes all Swift NIO-isms that were present in the public APIs (like EventLoopFuture and EventLoopGroup argument/return types). --- MIGRATION.md | 20 +- Package.resolved | 27 - Package.swift | 3 +- README.md | 13 +- Sources/GraphQL/Execution/Execute.swift | 423 +++---- Sources/GraphQL/GraphQL.swift | 161 +-- .../DispatchQueueInstrumentationWrapper.swift | 7 +- .../Instrumentation/Instrumentation.swift | 9 +- .../GraphQL/Subscription/EventStream.swift | 8 +- Sources/GraphQL/Subscription/Subscribe.swift | 116 +- Sources/GraphQL/Type/Definition.swift | 10 +- Sources/GraphQL/Type/Introspection.swift | 13 +- .../GraphQL/Utilities/NIO+Extensions.swift | 98 -- .../ExecutionTests/OneOfTests.swift | 51 +- .../FieldExecutionStrategyTests.swift | 121 +- .../HelloWorldTests/HelloWorldTests.swift | 40 +- .../GraphQLTests/InputTests/InputTests.swift | 1116 ++++++++--------- .../InstrumentationTests.swift | 5 +- .../StarWarsIntrospectionTests.swift | 127 +- .../StarWarsTests/StarWarsQueryTests.swift | 260 ++-- .../StarWarsTests/StarWarsSchema.swift | 6 +- .../SubscriptionSchema.swift | 25 +- .../SubscriptionTests/SubscriptionTests.swift | 203 ++- .../TypeTests/GraphQLSchemaTests.swift | 6 +- .../TypeTests/IntrospectionTests.swift | 20 +- .../GraphQLTests/TypeTests/ScalarTests.swift | 1 - .../UtilitiesTests/BuildASTSchemaTests.swift | 17 +- .../UtilitiesTests/ExtendSchemaTests.swift | 20 +- .../ValidationTests/ExampleSchema.swift | 18 +- 29 files changed, 1121 insertions(+), 1823 deletions(-) delete mode 100644 Sources/GraphQL/Utilities/NIO+Extensions.swift diff --git a/MIGRATION.md b/MIGRATION.md index b5c3d3ce..8ef81076 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,22 @@ # 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 + +### `ConcurrentDispatchFieldExecutionStrategy` + +This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. + +## 2 to 3 ### TypeReference removal @@ -73,4 +89,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..9bf74c2f 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"), ] ), diff --git a/README.md b/README.md index 87aeef25..06b7ae2f 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 }" ) ``` @@ -59,7 +58,7 @@ 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 +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 @@ -70,7 +69,7 @@ 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 return eventResult @@ -116,13 +115,13 @@ The example above assumes that your environment has access to Swift Concurrency. ## 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 +139,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/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index f082b721..005f3359 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -1,5 +1,4 @@ import Dispatch -import NIO import OrderedCollections /** @@ -37,7 +36,6 @@ public final class ExecutionContext { public let fragments: [String: FragmentDefinition] public let rootValue: Any public let context: Any - public let eventLoopGroup: EventLoopGroup public let operation: OperationDefinition public let variableValues: [String: Map] @@ -61,7 +59,6 @@ public final class ExecutionContext { fragments: [String: FragmentDefinition], rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, operation: OperationDefinition, variableValues: [String: Map], errors: [GraphQLError] @@ -74,7 +71,6 @@ public final class ExecutionContext { self.fragments = fragments self.rootValue = rootValue self.context = context - self.eventLoopGroup = eventLoopGroup self.operation = operation self.variableValues = variableValues _errors = errors @@ -96,7 +92,7 @@ public protocol FieldExecutionStrategy { sourceValue: Any, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> + ) async throws -> OrderedDictionary } public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {} @@ -117,30 +113,20 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, sourceValue: Any, path: IndexPath, fields: OrderedDictionary - ) throws -> Future> { + ) async throws -> OrderedDictionary { 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 } + 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 +135,38 @@ 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, 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?).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) } } @@ -239,10 +185,9 @@ func execute( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) -> Future { +) async throws -> GraphQLResult { let executeStarted = instrumentation.now let buildContext: ExecutionContext @@ -258,7 +203,6 @@ func execute( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, rawVariableValues: variableValues, operationName: operationName ) @@ -271,72 +215,42 @@ func execute( 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)]) } } @@ -355,7 +269,6 @@ func buildExecutionContext( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, rawVariableValues: [String: Map], operationName: String? ) throws -> ExecutionContext { @@ -410,7 +323,6 @@ func buildExecutionContext( fragments: fragments, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, operation: operation, variableValues: variableValues, errors: errors @@ -424,7 +336,7 @@ func executeOperation( exeContext: ExecutionContext, operation: OperationDefinition, rootValue: Any -) throws -> Future> { +) async throws -> OrderedDictionary { let type = try getOperationRootType(schema: exeContext.schema, operation: operation) var inputFields: OrderedDictionary = [:] var visitedFragmentNames: [String: Bool] = [:] @@ -448,7 +360,7 @@ func executeOperation( fieldExecutionStrategy = exeContext.subscriptionStrategy } - return try fieldExecutionStrategy.executeFields( + return try await fieldExecutionStrategy.executeFields( exeContext: exeContext, parentType: type, sourceValue: rootValue, @@ -687,7 +599,7 @@ public func resolveField( source: Any, fieldASTs: [Field], path: IndexPath -) throws -> Future { +) async throws -> Any? { let fieldAST = fieldASTs[0] let fieldName = fieldAST.name.value @@ -733,12 +645,11 @@ public func resolveField( // 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 ) @@ -749,12 +660,11 @@ public func resolveField( // 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, @@ -771,11 +681,10 @@ func resolveOrError( source: Any, args: Map, context: Any, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo -) -> Result, Error> { +) async -> Result { 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 +699,12 @@ func completeValueCatchingError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result +) async throws -> Any? { // 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 +717,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 +743,17 @@ func completeValueWithLocatedError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result +) async throws -> Any? { 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 +790,8 @@ func completeValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result, Error> -) throws -> Future { + result: Result +) async throws -> Any? { switch result { case let .failure(error): throw error @@ -901,79 +799,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)\"." + ) } } @@ -988,7 +882,7 @@ func completeListValue( info: GraphQLResolveInfo, path: IndexPath, result: Any -) throws -> Future<[Any?]> { +) async throws -> [Any?] { guard let result = result as? [Any?] else { throw GraphQLError( message: @@ -998,28 +892,32 @@ 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) + return try await withThrowingTaskGroup(of: (Int, Any?).self) { group in + // To preserve order, match size to result, and filter out nils at the end. + var results: [Any?] = 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) - let completedItem = try completeValueCatchingError( - exeContext: exeContext, - returnType: itemType, - fieldASTs: fieldASTs, - info: info, - path: fieldPath, - result: .success(futureItem) - ) - - 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) } /** @@ -1048,13 +946,12 @@ func completeAbstractValue( info: GraphQLResolveInfo, path: IndexPath, result: Any -) throws -> Future { - var resolveRes = try returnType.resolveType?(result, exeContext.eventLoopGroup, info) +) async throws -> Any? { + var resolveRes = try returnType.resolveType?(result, info) .typeResolveResult resolveRes = try resolveRes ?? defaultResolveType( value: result, - eventLoopGroup: exeContext.eventLoopGroup, info: info, abstractType: returnType ) @@ -1095,7 +992,7 @@ func completeAbstractValue( ) } - return try completeObjectValue( + return try await completeObjectValue( exeContext: exeContext, returnType: objectType, fieldASTs: fieldASTs, @@ -1115,13 +1012,13 @@ func completeObjectValue( info: GraphQLResolveInfo, path: IndexPath, result: Any -) throws -> Future { +) async throws -> Any? { // 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 +1043,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 } + ) } /** @@ -1162,7 +1059,6 @@ func completeObjectValue( */ func defaultResolveType( value: Any, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType ) throws -> TypeResolveResult? { @@ -1170,7 +1066,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 } @@ -1187,31 +1083,30 @@ func defaultResolve( source: Any, args _: Map, context _: Any, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo -) -> Future { +) async throws -> Any? { 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] { let value = subscriptable[info.fieldName] - return eventLoopGroup.next().makeSucceededFuture(value) + return value } 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..0488bb44 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? @@ -56,7 +55,7 @@ public struct SubscriptionResult { /// SubscriptionObservable represents an event stream of fully resolved GraphQL subscription /// results. Subscribers can be added to this stream. -public typealias SubscriptionEventStream = EventStream> +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 @@ -101,10 +100,9 @@ public func graphql( request: String, rootValue: Any = (), context: Any = (), - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) throws -> Future { +) async throws -> GraphQLResult { let source = Source(body: request, name: "GraphQL request") let documentAST = try parse(instrumentation: instrumentation, source: source) let validationErrors = validate( @@ -115,10 +113,10 @@ public func graphql( ) guard validationErrors.isEmpty else { - return eventLoopGroup.next().makeSucceededFuture(GraphQLResult(errors: validationErrors)) + return GraphQLResult(errors: validationErrors) } - return execute( + return try await execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -127,7 +125,6 @@ public func graphql( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) @@ -169,19 +166,18 @@ public func graphql( queryId: Retrieval.Id, rootValue: Any = (), context: Any = (), - eventLoopGroup: EventLoopGroup, 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( + return try await execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -190,7 +186,6 @@ public func graphql( documentAST: documentAST, rootValue: rootValue, context: context, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: operationName ) @@ -244,10 +239,9 @@ public func graphqlSubscribe( request: String, rootValue: Any = (), context: Any = (), - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) throws -> Future { +) async throws -> SubscriptionResult { let source = Source(body: request, name: "GraphQL Subscription request") let documentAST = try parse(instrumentation: instrumentation, source: source) let validationErrors = validate( @@ -258,11 +252,10 @@ public func graphqlSubscribe( ) guard validationErrors.isEmpty else { - return eventLoopGroup.next() - .makeSucceededFuture(SubscriptionResult(errors: validationErrors)) + return SubscriptionResult(errors: validationErrors) } - return subscribe( + return try await subscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -271,141 +264,7 @@ public func graphqlSubscribe( 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/Instrumentation/DispatchQueueInstrumentationWrapper.swift b/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift index 73d54278..f58ce3f5 100644 --- a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift +++ b/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift @@ -1,5 +1,4 @@ import Dispatch -import NIO /// Proxies calls through to another `Instrumentation` instance via a DispatchQueue /// @@ -89,7 +88,6 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { schema: GraphQLSchema, document: Document, rootValue: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map], operation: OperationDefinition?, errors: [GraphQLError], @@ -104,7 +102,6 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { schema: schema, document: document, rootValue: rootValue, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operation: operation, errors: errors, @@ -120,9 +117,8 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { finished: DispatchTime, source: Any, args: Map, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, - result: Result, Error> + result: Result ) { dispatchQueue.async(group: dispatchGroup) { self.instrumentation.fieldResolution( @@ -132,7 +128,6 @@ public class DispatchQueueInstrumentationWrapper: Instrumentation { 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 index 1a315ab0..90fd04b0 100644 --- a/Sources/GraphQL/Instrumentation/Instrumentation.swift +++ b/Sources/GraphQL/Instrumentation/Instrumentation.swift @@ -1,6 +1,5 @@ import Dispatch import Foundation -import NIO /// Provides the capability to instrument the execution steps of a GraphQL query. /// @@ -35,7 +34,6 @@ public protocol Instrumentation { schema: GraphQLSchema, document: Document, rootValue: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map], operation: OperationDefinition?, errors: [GraphQLError], @@ -49,9 +47,8 @@ public protocol Instrumentation { finished: DispatchTime, source: Any, args: Map, - eventLoopGroup: EventLoopGroup, info: GraphQLResolveInfo, - result: Result, Error> + result: Result ) } @@ -105,7 +102,6 @@ struct noOpInstrumentation: Instrumentation { schema _: GraphQLSchema, document _: Document, rootValue _: Any, - eventLoopGroup _: EventLoopGroup, variableValues _: [String: Map], operation _: OperationDefinition?, errors _: [GraphQLError], @@ -119,8 +115,7 @@ struct noOpInstrumentation: Instrumentation { finished _: DispatchTime, source _: Any, args _: Map, - eventLoopGroup _: EventLoopGroup, info _: GraphQLResolveInfo, - result _: Result, Error> + result _: Result ) {} } diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift index 5ff7ad89..2f78093c 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -3,7 +3,7 @@ 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 { + open func map(_: @escaping (Element) async throws -> To) -> EventStream { fatalError("This function should be overridden by implementing classes") } } @@ -21,7 +21,7 @@ public class ConcurrentEventStream: EventStream { /// 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) + override open func map(_ closure: @escaping (Element) async throws -> To) -> ConcurrentEventStream { let newStream = stream.mapStream(closure) return ConcurrentEventStream(newStream) @@ -30,13 +30,13 @@ public class ConcurrentEventStream: EventStream { @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) extension AsyncThrowingStream { - func mapStream(_ closure: @escaping (Element) throws -> To) + func mapStream(_ closure: @escaping (Element) async throws -> To) -> AsyncThrowingStream { return AsyncThrowingStream { continuation in let task = Task { do { for try await event in self { - let newEvent = try closure(event) + let newEvent = try await closure(event) continuation.yield(newEvent) } continuation.finish() diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 8236b3d5..30dd61cb 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -1,4 +1,3 @@ -import NIO import OrderedCollections /** @@ -30,11 +29,10 @@ func subscribe( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) -> EventLoopFuture { - let sourceFuture = createSourceEventStream( +) async throws -> SubscriptionResult { + let sourceResult = try await createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, @@ -43,38 +41,34 @@ func subscribe( 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 SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) - } else { - return SubscriptionResult(errors: sourceResult.errors) + if let sourceStream = sourceResult.stream { + let subscriptionStream = sourceStream.map { eventPayload -> GraphQLResult 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. + try await execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: eventPayload, + context: context, + variableValues: variableValues, + operationName: operationName + ) } + return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) + } else { + return SubscriptionResult(errors: sourceResult.errors) } } @@ -114,10 +108,9 @@ func createSourceEventStream( documentAST: Document, rootValue: Any, context: Any, - eventLoopGroup: EventLoopGroup, variableValues: [String: Map] = [:], operationName: String? = nil -) -> EventLoopFuture { +) async throws -> SourceEventStreamResult { let executeStarted = instrumentation.now do { @@ -132,11 +125,10 @@ func createSourceEventStream( 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(), @@ -146,24 +138,21 @@ func createSourceEventStream( schema: schema, document: documentAST, rootValue: rootValue, - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operation: nil, errors: [error], result: nil ) - return eventLoopGroup.next().makeSucceededFuture(SourceEventStreamResult(errors: [error])) + return SourceEventStreamResult(errors: [error]) } catch { - return eventLoopGroup.next() - .makeSucceededFuture(SourceEventStreamResult(errors: [GraphQLError(error)])) + return SourceEventStreamResult(errors: [GraphQLError(error)]) } } func executeSubscription( context: ExecutionContext, - eventLoopGroup: EventLoopGroup -) throws -> EventLoopFuture { +) async throws -> SourceEventStreamResult { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) var inputFields: OrderedDictionary = [:] @@ -233,17 +222,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,27 +239,25 @@ 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)'" - ), - ]) - } + 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)'" + ), + ]) } } diff --git a/Sources/GraphQL/Type/Definition.swift b/Sources/GraphQL/Type/Definition.swift index 7f2c0924..409d8344 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -1,5 +1,4 @@ import Foundation -import NIO import OrderedCollections /** @@ -414,13 +413,11 @@ public enum TypeResolveResult { public typealias GraphQLTypeResolve = ( _ value: Any, - _ eventLoopGroup: EventLoopGroup, _ info: GraphQLResolveInfo ) throws -> TypeResolveResultRepresentable public typealias GraphQLIsTypeOf = ( _ source: Any, - _ eventLoopGroup: EventLoopGroup, _ info: GraphQLResolveInfo ) throws -> Bool @@ -428,9 +425,8 @@ public typealias GraphQLFieldResolve = ( _ source: Any, _ args: Map, _ context: Any, - _ eventLoopGroup: EventLoopGroup, _ info: GraphQLResolveInfo -) throws -> Future +) async throws -> Any? public typealias GraphQLFieldResolveInput = ( _ source: Any, @@ -511,9 +507,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 } diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index 9841851d..89c2cc2f 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", @@ -492,8 +491,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 +506,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 +516,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/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/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 index cd021f3b..cf8e09e1 100644 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift @@ -1,6 +1,5 @@ import Dispatch @testable import GraphQL -import NIO import XCTest class FieldExecutionStrategyTests: XCTestCase { @@ -14,16 +13,14 @@ class FieldExecutionStrategyTests: XCTestCase { fields: [ "sleep": GraphQLField( type: GraphQLString, - resolve: { _, _, _, eventLoopGroup, _ in - eventLoopGroup.next().makeSucceededVoidFuture().map { - Thread.sleep(forTimeInterval: 0.1) - return "z" - } + resolve: { _, _, _, _ in + Thread.sleep(forTimeInterval: 0.1) + return "z" } ), "bang": GraphQLField( type: GraphQLString, - resolve: { (_, _, _, _, info: GraphQLResolveInfo) in + resolve: { (_, _, _, info: GraphQLResolveInfo) in let group = DispatchGroup() group.enter() @@ -40,7 +37,7 @@ class FieldExecutionStrategyTests: XCTestCase { ), "futureBang": GraphQLField( type: GraphQLString, - resolve: { (_, _, _, eventLoopGroup, info: GraphQLResolveInfo) in + resolve: { (_, _, _, info: GraphQLResolveInfo) in let g = DispatchGroup() g.enter() @@ -50,9 +47,9 @@ class FieldExecutionStrategyTests: XCTestCase { g.wait() - return eventLoopGroup.next().makeFailedFuture(StrategyError.exampleError( + throw StrategyError.exampleError( msg: "\(info.fieldName): \(info.path.elements.last!)" - )) + ) } ), ] @@ -184,9 +181,10 @@ class FieldExecutionStrategyTests: XCTestCase { ), ] - func timing(_ block: @autoclosure () throws -> T) throws -> (value: T, seconds: Double) { + func timing(_ block: @autoclosure () async throws -> T) async throws + -> (value: T, seconds: Double) { let start = DispatchTime.now() - let value = try block() + let value = try await block() let nanoseconds = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds let seconds = Double(nanoseconds) / 1_000_000_000 return ( @@ -195,67 +193,52 @@ class FieldExecutionStrategyTests: XCTestCase { ) } - 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( + func testSerialFieldExecutionStrategyWithSingleField() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: singleQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleQuery + )) XCTAssertEqual(result.value, singleExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testSerialFieldExecutionStrategyWithSingleFieldError() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithSingleFieldError() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: singleThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleThrowsQuery + )) XCTAssertEqual(result.value, singleThrowsExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testSerialFieldExecutionStrategyWithSingleFieldFailedFuture() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithSingleFieldFailedFuture() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: singleFailedFutureQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleFailedFutureQuery + )) XCTAssertEqual(result.value, singleFailedFutureExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testSerialFieldExecutionStrategyWithMultipleFields() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithMultipleFields() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: multiQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiQuery + )) XCTAssertEqual(result.value, multiExpected) // XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) } - func testSerialFieldExecutionStrategyWithMultipleFieldErrors() throws { - let result = try timing(graphql( + func testSerialFieldExecutionStrategyWithMultipleFieldErrors() async throws { + let result = try await timing(await graphql( queryStrategy: SerialFieldExecutionStrategy(), schema: schema, - request: multiThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiThrowsQuery + )) XCTAssertEqual(result.value.data, multiThrowsExpectedData) let resultErrors = result.value.errors XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) @@ -265,46 +248,42 @@ class FieldExecutionStrategyTests: XCTestCase { // XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) } - func testConcurrentDispatchFieldExecutionStrategyWithSingleField() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithSingleField() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: singleQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleQuery + )) XCTAssertEqual(result.value, singleExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testConcurrentDispatchFieldExecutionStrategyWithSingleFieldError() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithSingleFieldError() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: singleThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: singleThrowsQuery + )) XCTAssertEqual(result.value, singleThrowsExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testConcurrentDispatchFieldExecutionStrategyWithMultipleFields() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithMultipleFields() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: multiQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiQuery + )) XCTAssertEqual(result.value, multiExpected) // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) } - func testConcurrentDispatchFieldExecutionStrategyWithMultipleFieldErrors() throws { - let result = try timing(graphql( - queryStrategy: ConcurrentDispatchFieldExecutionStrategy(), + func testConcurrentFieldExecutionStrategyWithMultipleFieldErrors() async throws { + let result = try await timing(await graphql( + queryStrategy: ConcurrentFieldExecutionStrategy(), schema: schema, - request: multiThrowsQuery, - eventLoopGroup: eventLoopGroup - ).wait()) + request: multiThrowsQuery + )) XCTAssertEqual(result.value.data, multiThrowsExpectedData) let resultErrors = result.value.errors XCTAssertEqual(resultErrors.count, multiThrowsExpectedErrors.count) diff --git a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift index de6252ea..c4da2977 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,22 @@ 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 index 56a77b93..3583aa7f 100644 --- a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift +++ b/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift @@ -1,7 +1,6 @@ import Dispatch import Foundation import GraphQL -import NIO import XCTest class InstrumentationTests: XCTestCase, Instrumentation { @@ -101,7 +100,6 @@ class InstrumentationTests: XCTestCase, Instrumentation { schema _: GraphQLSchema, document _: Document, rootValue _: Any, - eventLoopGroup _: EventLoopGroup, variableValues _: [String: Map], operation _: OperationDefinition?, errors _: [GraphQLError], @@ -128,9 +126,8 @@ class InstrumentationTests: XCTestCase, Instrumentation { finished _: DispatchTime, source _: Any, args _: Map, - eventLoopGroup _: EventLoopGroup, info _: GraphQLResolveInfo, - result _: Result, Error> + result _: Result ) { fieldResolutionCalled += 1 // XCTAssertEqual(processId, expectedProcessId, "unexpected process 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/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index fb940c72..b535fc43 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -1,5 +1,4 @@ @testable import GraphQL -import NIO // MARK: Types @@ -89,8 +88,6 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -let eventLoopGroup = MultiThreadedEventLoopGroup(numberOfThreads: 1) - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class EmailDb { var emails: [Email] @@ -121,17 +118,17 @@ class EmailDb { /// 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 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 @@ -141,8 +138,7 @@ class EmailDb { return true } } - return eventLoopGroup.next() - .makeSucceededFuture(ConcurrentEventStream(filtered)) + return ConcurrentEventStream(filtered) } ) } @@ -151,8 +147,8 @@ class EmailDb { func subscription( query: String, variableValues: [String: Map] = [:] - ) throws -> SubscriptionEventStream { - return try createSubscription( + ) async throws -> SubscriptionEventStream { + return try await createSubscription( schema: defaultSchema(), query: query, variableValues: variableValues @@ -191,8 +187,8 @@ func createSubscription( schema: GraphQLSchema, query: String, variableValues: [String: Map] = [:] -) throws -> SubscriptionEventStream { - let result = try graphqlSubscribe( +) async throws -> SubscriptionEventStream { + let result = try await graphqlSubscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), subscriptionStrategy: SerialFieldExecutionStrategy(), @@ -201,10 +197,9 @@ func createSubscription( request: query, rootValue: (), context: (), - eventLoopGroup: eventLoopGroup, variableValues: variableValues, operationName: nil - ).wait() + ) if let stream = result.stream { return stream diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index db0ebfb6..fc52ffea 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -1,5 +1,4 @@ import GraphQL -import NIO import XCTest /// This follows the graphql-js testing, with deviations where noted. @@ -30,9 +29,8 @@ class SubscriptionTests: XCTestCase { let subscriptionResult = try await graphqlSubscribe( schema: schema, - request: query, - eventLoopGroup: eventLoopGroup - ).get() + request: query + ) guard let subscription = subscriptionResult.stream else { XCTFail(subscriptionResult.errors.description) return @@ -50,7 +48,7 @@ class SubscriptionTests: XCTestCase { unread: true )) db.stop() - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -85,23 +83,19 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + resolve: { emailAny, _, _, _ throws -> Any? in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" ) } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, inbox: Inbox(emails: db.emails) - )) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws -> Any? in + db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( @@ -111,29 +105,25 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + resolve: { emailAny, _, _, _ throws -> Any? in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" ) } - return eventLoopGroup.next().makeSucceededFuture(EmailEvent( + return EmailEvent( email: email, inbox: Inbox(emails: db.emails) - )) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws -> Any? in + db.publisher.subscribe() } ), ] ) ) - let subscription = try createSubscription(schema: schema, query: """ + let subscription = try await createSubscription(schema: schema, query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -160,7 +150,7 @@ class SubscriptionTests: XCTestCase { unread: true )) - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -195,34 +185,28 @@ class SubscriptionTests: XCTestCase { fields: [ "importantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ throws -> Any? in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + subscribe: { _, _, _, _ throws -> Any? in didResolveImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) + return db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(nil) + resolve: { _, _, _, _ throws -> Any? in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture< - Any? - > in + subscribe: { _, _, _, _ throws -> Any? in didResolveNonImportantEmail = true - return eventLoopGroup.next() - .makeSucceededFuture(db.publisher.subscribe()) + return db.publisher.subscribe() } ), ] ) ) - let _ = try createSubscription(schema: schema, query: """ + let _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -256,15 +240,16 @@ 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 + XCTFail("Error should have been thrown") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -278,25 +263,26 @@ class SubscriptionTests: XCTestCase { } /// '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 -> Any? in + nil }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture("test") + subscribe: { _, _, _, _ throws -> Any? in + "test" } ) - XCTAssertThrowsError( - try createSubscription(schema: schema, query: """ + do { + _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -305,7 +291,8 @@ class SubscriptionTests: XCTestCase { } } """) - ) { error in + XCTFail("Error should have been thrown") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -318,10 +305,10 @@ class SubscriptionTests: XCTestCase { } /// '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,7 +317,8 @@ class SubscriptionTests: XCTestCase { } } """) - ) { error in + XCTFail("Error should have been thrown") + } catch { guard let graphQLError = error as? GraphQLError else { XCTFail("Error was not of type GraphQLError") return @@ -340,23 +328,23 @@ class SubscriptionTests: XCTestCase { } // Throwing an error - try verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _, _ throws -> EventLoopFuture in + try await verifyError(schema: emailSchemaWithResolvers( + subscribe: { _, _, _, _ throws -> Any? 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 -> Any? 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 -> Any? in + GraphQLError(message: "test error") } )) } @@ -365,7 +353,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 +370,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 +395,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 subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -434,7 +423,7 @@ class SubscriptionTests: XCTestCase { )) db.stop() - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( @@ -455,7 +444,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 subscription1 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -474,7 +463,7 @@ class SubscriptionTests: XCTestCase { return } - let subscription2 = try db.subscription(query: """ + let subscription2 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -503,8 +492,8 @@ class SubscriptionTests: XCTestCase { 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 +515,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload per subscription event' func testPayloadPerEvent() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -553,7 +542,7 @@ class SubscriptionTests: XCTestCase { message: "Tests are good", unread: true )) - let result1 = try await iterator.next()?.get() + let result1 = try await iterator.next() XCTAssertEqual( result1, GraphQLResult( @@ -577,7 +566,7 @@ class SubscriptionTests: XCTestCase { 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 +588,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 subscription = try await db.subscription(query: """ subscription ($priority: Int = 5) { importantEmail(priority: $priority) { email { @@ -623,11 +612,9 @@ class SubscriptionTests: XCTestCase { // 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() - } + let keepForNow = stream.map { result in + results.append(result) + expectation.fulfill() } var expected = [GraphQLResult]() @@ -703,7 +690,7 @@ class SubscriptionTests: XCTestCase { /// 'should not trigger when subscription is already done' func testNoTriggerAfterDone() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -726,11 +713,9 @@ class SubscriptionTests: XCTestCase { 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() - } + let keepForNow = stream.map { result in + results.append(result) + expectation.fulfill() } var expected = [GraphQLResult]() @@ -784,7 +769,7 @@ class SubscriptionTests: XCTestCase { /// 'event order is correct for multiple publishes' func testOrderCorrectForMultiplePublishes() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -817,7 +802,7 @@ class SubscriptionTests: XCTestCase { unread: true )) - let result1 = try await iterator.next()?.get() + let result1 = try await iterator.next() XCTAssertEqual( result1, GraphQLResult( @@ -834,7 +819,7 @@ class SubscriptionTests: XCTestCase { ) ) - let result2 = try await iterator.next()?.get() + let result2 = try await iterator.next() XCTAssertEqual( result2, GraphQLResult( @@ -857,7 +842,7 @@ class SubscriptionTests: XCTestCase { let db = EmailDb() let schema = try emailSchemaWithResolvers( - resolve: { emailAny, _, _, eventLoopGroup, _ throws -> EventLoopFuture in + resolve: { emailAny, _, _, _ throws -> Any? in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" @@ -866,17 +851,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) - )) + ) }, - subscribe: { _, _, _, eventLoopGroup, _ throws -> EventLoopFuture in - eventLoopGroup.next().makeSucceededFuture(db.publisher.subscribe()) + subscribe: { _, _, _, _ throws -> Any? in + db.publisher.subscribe() } ) - let subscription = try createSubscription(schema: schema, query: """ + let subscription = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -894,11 +879,9 @@ class SubscriptionTests: XCTestCase { 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() - } + let keepForNow = stream.map { result in + results.append(result) + expectation.fulfill() } var expected = [GraphQLResult]() @@ -971,7 +954,7 @@ class SubscriptionTests: XCTestCase { /// Test incorrect emitted type errors func testErrorWrongEmitType() async throws { let db = EmailDb() - let subscription = try db.subscription(query: """ + let subscription = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -993,7 +976,7 @@ class SubscriptionTests: XCTestCase { db.publisher.emit(event: "String instead of email") - let result = try await iterator.next()?.get() + let result = try await iterator.next() XCTAssertEqual( result, GraphQLResult( 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..91429598 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, 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/ValidationTests/ExampleSchema.swift b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift index 2350c285..d0571f94 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" } ) @@ -293,7 +293,7 @@ let ValidationExampleSentient = try! GraphQLInterfaceType( return nil }, ], - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" } ) @@ -363,7 +363,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 +372,7 @@ let ValidationExampleDogOrHuman = try! GraphQLUnionType( // union HumanOrAlien = Human | Alien let ValidationExampleHumanOrAlien = try! GraphQLUnionType( name: "HumanOrAlien", - resolveType: { _, _, _ in + resolveType: { _, _ in "Unknown" }, types: [ValidationExampleHuman, ValidationExampleAlien] From d112be0f913ed6702f596ee23878b576ff8814ef Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 24 Jun 2025 14:46:39 -0600 Subject: [PATCH 02/20] chore: Removes unnecessary @available --- Sources/GraphQL/Map/AnyCoder.swift | 4 ---- Sources/GraphQL/Map/GraphQLJSONEncoder.swift | 3 --- Sources/GraphQL/Map/MapCoder.swift | 4 ---- Sources/GraphQL/Map/Number.swift | 4 ---- Sources/GraphQL/Subscription/EventStream.swift | 2 -- Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift | 1 - Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift | 1 - Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift | 1 - Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift | 1 - 9 files changed, 21 deletions(-) diff --git a/Sources/GraphQL/Map/AnyCoder.swift b/Sources/GraphQL/Map/AnyCoder.swift index dcab5052..debf273d 100644 --- a/Sources/GraphQL/Map/AnyCoder.swift +++ b/Sources/GraphQL/Map/AnyCoder.swift @@ -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. @@ -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. @@ -3258,7 +3255,6 @@ 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 = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime diff --git a/Sources/GraphQL/Map/GraphQLJSONEncoder.swift b/Sources/GraphQL/Map/GraphQLJSONEncoder.swift index a938ee4f..cb6029a7 100644 --- a/Sources/GraphQL/Map/GraphQLJSONEncoder.swift +++ b/Sources/GraphQL/Map/GraphQLJSONEncoder.swift @@ -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. @@ -1298,7 +1296,6 @@ 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 = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime diff --git a/Sources/GraphQL/Map/MapCoder.swift b/Sources/GraphQL/Map/MapCoder.swift index aabcac69..cc334dda 100644 --- a/Sources/GraphQL/Map/MapCoder.swift +++ b/Sources/GraphQL/Map/MapCoder.swift @@ -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. @@ -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. @@ -3266,7 +3263,6 @@ 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 = { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime 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 index 2f78093c..aad15123 100644 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ b/Sources/GraphQL/Subscription/EventStream.swift @@ -9,7 +9,6 @@ open class EventStream { } /// 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 @@ -28,7 +27,6 @@ public class ConcurrentEventStream: EventStream { } } -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) extension AsyncThrowingStream { func mapStream(_ closure: @escaping (Element) async throws -> To) -> AsyncThrowingStream { diff --git a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift index c4da2977..3131c600 100644 --- a/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift +++ b/Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift @@ -48,7 +48,6 @@ class HelloWorldTests: XCTestCase { XCTAssertEqual(result, expected) } - @available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) func testHelloAsync() async throws { let query = "{ hello }" let expected = GraphQLResult(data: ["hello": "world"]) diff --git a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index 8a742378..e33f1ba4 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -1,7 +1,6 @@ import GraphQL /// A very simple publish/subscriber used for testing -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class SimplePubSub { private var subscribers: [Subscriber] diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index b535fc43..ad4cf3a8 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -88,7 +88,6 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -@available(macOS 10.15, iOS 15, watchOS 8, tvOS 15, *) class EmailDb { var emails: [Email] let publisher: SimplePubSub diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index fc52ffea..f429a276 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -2,7 +2,6 @@ import GraphQL 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 From 9ec313a8730ecd8b2d7c3768ea0d0791d018b786 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 24 Jun 2025 16:09:57 -0600 Subject: [PATCH 03/20] feat!: Removes EventStream, replacing with AsyncThrowingStream --- MIGRATION.md | 4 + README.md | 8 +- Sources/GraphQL/GraphQL.swift | 11 +- .../GraphQL/Subscription/EventStream.swift | 73 --------- Sources/GraphQL/Subscription/Subscribe.swift | 65 +++++--- .../SubscriptionTests/SimplePubSub.swift | 5 +- .../SubscriptionSchema.swift | 19 ++- .../SubscriptionTests/SubscriptionTests.swift | 153 ++++-------------- 8 files changed, 94 insertions(+), 244 deletions(-) delete mode 100644 Sources/GraphQL/Subscription/EventStream.swift diff --git a/MIGRATION.md b/MIGRATION.md index 8ef81076..1795c5ad 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -16,6 +16,10 @@ The documentation here will be very helpful in the conversion: https://www.swift This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. +### 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. + ## 2 to 3 ### TypeReference removal diff --git a/README.md b/README.md index 06b7ae2f..8f94cb5b 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ let schema = try GraphQLSchema( return eventResult }, subscribe: { _, _, _, _, _ in // Defines how to construct the event stream - let asyncStream = AsyncThrowingStream { continuation in + return AsyncThrowingStream { continuation in let timer = Timer.scheduledTimer( withTimeInterval: 3, repeats: true, @@ -83,7 +83,6 @@ let schema = try GraphQLSchema( continuation.yield("world") // Emits "world" every 3 seconds } } - return ConcurrentEventStream(asyncStream) } ) ] @@ -97,8 +96,6 @@ 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 { print(result) } @@ -110,9 +107,6 @@ 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, diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 0488bb44..24f26eb4 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -44,19 +44,18 @@ 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? + public let stream: AsyncThrowingStream? public let errors: [GraphQLError] - public init(stream: SubscriptionEventStream? = nil, errors: [GraphQLError] = []) { + public init( + stream: AsyncThrowingStream? = nil, + errors: [GraphQLError] = [] + ) { self.stream = stream 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. diff --git a/Sources/GraphQL/Subscription/EventStream.swift b/Sources/GraphQL/Subscription/EventStream.swift deleted file mode 100644 index aad15123..00000000 --- a/Sources/GraphQL/Subscription/EventStream.swift +++ /dev/null @@ -1,73 +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) async throws -> To) -> EventStream { - fatalError("This function should be overridden by implementing classes") - } -} - -/// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system. -public class ConcurrentEventStream: EventStream { - public let stream: AsyncThrowingStream - - public init(_ stream: AsyncThrowingStream) { - self.stream = stream - } - - /// 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) async throws -> To) - -> ConcurrentEventStream { - let newStream = stream.mapStream(closure) - return ConcurrentEventStream(newStream) - } -} - -extension AsyncThrowingStream { - func mapStream(_ closure: @escaping (Element) async throws -> To) - -> AsyncThrowingStream { - return AsyncThrowingStream { continuation in - let task = Task { - do { - for try await event in self { - let newEvent = try await 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 30dd61cb..ac9f44db 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -46,25 +46,42 @@ func subscribe( ) if let sourceStream = sourceResult.stream { - let subscriptionStream = sourceStream.map { eventPayload -> GraphQLResult 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. - try await execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, - schema: schema, - documentAST: documentAST, - rootValue: eventPayload, - context: context, - variableValues: variableValues, - operationName: operationName - ) + // We must create a new AsyncSequence because AsyncSequence.map requires a concrete type + // (which we cannot know), + // and we need the result to be a concrete type. + let subscriptionStream = AsyncThrowingStream { continuation in + let task = Task { + do { + for try await eventPayload in sourceStream { + // 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. + let newEvent = try await execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + instrumentation: instrumentation, + schema: schema, + documentAST: documentAST, + rootValue: eventPayload, + context: context, + variableValues: variableValues, + operationName: operationName + ) + continuation.yield(newEvent) + } + continuation.finish() + } catch { + continuation.finish(throwing: error) + } + } + + continuation.onTermination = { @Sendable reason in + task.cancel() + } } return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) } else { @@ -151,7 +168,7 @@ func createSourceEventStream( } func executeSubscription( - context: ExecutionContext, + context: ExecutionContext ) async throws -> SourceEventStreamResult { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) @@ -245,7 +262,7 @@ func executeSubscription( return SourceEventStreamResult(errors: context.errors) } else if let error = resolved as? GraphQLError { return SourceEventStreamResult(errors: [error]) - } else if let stream = resolved as? EventStream { + } else if let stream = resolved as? any AsyncSequence { return SourceEventStreamResult(stream: stream) } else if resolved == nil { return SourceEventStreamResult(errors: [ @@ -255,7 +272,7 @@ func executeSubscription( let resolvedObj = resolved as AnyObject return SourceEventStreamResult(errors: [ GraphQLError( - message: "Subscription field resolver must return EventStream. Received: '\(resolvedObj)'" + message: "Subscription field resolver must return an AsyncSequence. Received: '\(resolvedObj)'" ), ]) } @@ -266,10 +283,10 @@ func executeSubscription( // checking. Normal resolvers for subscription fields should handle type casting, same as resolvers // for query fields. struct SourceEventStreamResult { - public let stream: EventStream? + public let stream: (any AsyncSequence)? public let errors: [GraphQLError] - public init(stream: EventStream? = nil, errors: [GraphQLError] = []) { + public init(stream: (any AsyncSequence)? = nil, errors: [GraphQLError] = []) { self.stream = stream self.errors = errors } diff --git a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index e33f1ba4..fb198bfa 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -20,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) @@ -32,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 ad4cf3a8..53d4bab1 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -129,15 +129,14 @@ class EmailDb { }, 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 - } + let filtered = self.publisher.subscribe().filter { emailAny throws in + if let email = emailAny as? Email { + return email.priority >= priority + } else { + return true } - return ConcurrentEventStream(filtered) + } + return filtered } ) } @@ -146,7 +145,7 @@ class EmailDb { func subscription( query: String, variableValues: [String: Map] = [:] - ) async throws -> SubscriptionEventStream { + ) async throws -> AsyncThrowingStream { return try await createSubscription( schema: defaultSchema(), query: query, @@ -186,7 +185,7 @@ func createSubscription( schema: GraphQLSchema, query: String, variableValues: [String: Map] = [:] -) async throws -> SubscriptionEventStream { +) async throws -> AsyncThrowingStream { let result = try await graphqlSubscribe( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index f429a276..8711991b 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -30,15 +30,11 @@ class SubscriptionTests: XCTestCase { schema: schema, request: query ) - guard let subscription = subscriptionResult.stream else { + guard let stream = 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() + var iterator = stream.makeAsyncIterator() db.trigger(email: Email( from: "yuzhi@graphql.org", @@ -122,7 +118,7 @@ class SubscriptionTests: XCTestCase { ] ) ) - let subscription = try await createSubscription(schema: schema, query: """ + let stream = try await createSubscription(schema: schema, query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -136,11 +132,7 @@ 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( from: "yuzhi@graphql.org", @@ -298,7 +290,7 @@ class SubscriptionTests: XCTestCase { } XCTAssertEqual( graphQLError.message, - "Subscription field resolver must return EventStream. Received: 'test'" + "Subscription field resolver must return an AsyncSequence. Received: 'test'" ) } } @@ -394,7 +386,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for a single subscriber' func testSingleSubscriber() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -408,11 +400,7 @@ 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( from: "yuzhi@graphql.org", @@ -443,7 +431,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload for multiple subscribe in same subscription' func testMultipleSubscribers() async throws { let db = EmailDb() - let subscription1 = try await db.subscription(query: """ + let stream1 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -457,12 +445,8 @@ class SubscriptionTests: XCTestCase { } } """) - guard let stream1 = subscription1 as? ConcurrentEventStream else { - XCTFail("stream isn't ConcurrentEventStream") - return - } - let subscription2 = try await db.subscription(query: """ + let stream2 = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -476,13 +460,9 @@ 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( from: "yuzhi@graphql.org", @@ -514,7 +494,7 @@ class SubscriptionTests: XCTestCase { /// 'produces a payload per subscription event' func testPayloadPerEvent() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -528,11 +508,7 @@ 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( @@ -587,7 +563,7 @@ class SubscriptionTests: XCTestCase { /// This is not in the graphql-js tests. func testArguments() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 5) { importantEmail(priority: $priority) { email { @@ -601,21 +577,8 @@ 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 { result in - results.append(result) - expectation.fulfill() - } - + var iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() db.trigger(email: Email( @@ -639,12 +602,10 @@ 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( from: "hyo@graphql.org", subject: "Not Important", @@ -652,11 +613,9 @@ class SubscriptionTests: XCTestCase { unread: true, priority: 2 )) - wait(for: [expectation], timeout: timeoutDuration) XCTAssertEqual(results, expected) // Higher priority one should trigger again - expectation = XCTestExpectation() db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", @@ -678,18 +637,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 await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -703,19 +658,8 @@ 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 { result in - results.append(result) - expectation.fulfill() - } + var iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() db.trigger(email: Email( @@ -738,28 +682,20 @@ class SubscriptionTests: XCTestCase { ]] ) ) - wait(for: [expectation], timeout: timeoutDuration) + try await results.append(iterator.next()) XCTAssertEqual(results, expected) db.stop() // This should not trigger an event. - expectation = XCTestExpectation() - expectation.isInverted = true db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", unread: true )) - // Ensure that the current result was the one before the db was stopped - wait(for: [expectation], timeout: timeoutDuration) XCTAssertEqual(results, expected) - - // So that the Task won't immediately be cancelled since the ConcurrentEventStream is - // discarded - _ = keepForNow } /// 'should not trigger when subscription is thrown' @@ -768,7 +704,7 @@ class SubscriptionTests: XCTestCase { /// 'event order is correct for multiple publishes' func testOrderCorrectForMultiplePublishes() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -782,11 +718,7 @@ 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( from: "yuzhi@graphql.org", @@ -860,7 +792,7 @@ class SubscriptionTests: XCTestCase { } ) - let subscription = try await createSubscription(schema: schema, query: """ + let stream = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -869,19 +801,8 @@ 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 { result in - results.append(result) - expectation.fulfill() - } + var iterator = stream.makeAsyncIterator() + var results = [GraphQLResult?]() var expected = [GraphQLResult]() db.trigger(email: Email( @@ -899,10 +820,9 @@ 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( from: "yuzhi@graphql.org", @@ -918,10 +838,9 @@ 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( @@ -939,12 +858,8 @@ 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' @@ -953,7 +868,7 @@ class SubscriptionTests: XCTestCase { /// Test incorrect emitted type errors func testErrorWrongEmitType() async throws { let db = EmailDb() - let subscription = try await db.subscription(query: """ + let stream = try await db.subscription(query: """ subscription ($priority: Int = 0) { importantEmail(priority: $priority) { email { @@ -967,11 +882,7 @@ 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.publisher.emit(event: "String instead of email") From 9b4a6f27fee56f8f4a5188dd1ee20572a6a91127 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 26 Jun 2025 18:22:10 -0600 Subject: [PATCH 04/20] feat!: Removes Instrumentation The intent is to replace it with swift-distributed-tracing integration. --- MIGRATION.md | 4 + Sources/GraphQL/Execution/Execute.swift | 35 ---- Sources/GraphQL/GraphQL.swift | 18 +- Sources/GraphQL/GraphQLRequest.swift | 1 - .../DispatchQueueInstrumentationWrapper.swift | 136 ------------ .../Instrumentation/Instrumentation.swift | 121 ----------- Sources/GraphQL/Language/Parser.swift | 20 -- Sources/GraphQL/Subscription/Subscribe.swift | 21 -- Sources/GraphQL/Validation/Validate.swift | 15 +- .../InstrumentationTests.swift | 194 ------------------ .../SubscriptionSchema.swift | 1 - 11 files changed, 7 insertions(+), 559 deletions(-) delete mode 100644 Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift delete mode 100644 Sources/GraphQL/Instrumentation/Instrumentation.swift delete mode 100644 Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift diff --git a/MIGRATION.md b/MIGRATION.md index 1795c5ad..325fc0cd 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,6 +20,10 @@ This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. 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. +### 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. + ## 2 to 3 ### TypeReference removal diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 005f3359..403b65ab 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -31,7 +31,6 @@ public final class ExecutionContext { let queryStrategy: QueryFieldExecutionStrategy let mutationStrategy: MutationFieldExecutionStrategy let subscriptionStrategy: SubscriptionFieldExecutionStrategy - let instrumentation: Instrumentation public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] public let rootValue: Any @@ -54,7 +53,6 @@ public final class ExecutionContext { queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, fragments: [String: FragmentDefinition], rootValue: Any, @@ -66,7 +64,6 @@ public final class ExecutionContext { self.queryStrategy = queryStrategy self.mutationStrategy = mutationStrategy self.subscriptionStrategy = subscriptionStrategy - self.instrumentation = instrumentation self.schema = schema self.fragments = fragments self.rootValue = rootValue @@ -180,7 +177,6 @@ func execute( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -188,7 +184,6 @@ func execute( variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> GraphQLResult { - let executeStarted = instrumentation.now let buildContext: ExecutionContext do { @@ -198,7 +193,6 @@ func execute( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -207,20 +201,6 @@ func execute( operationName: operationName ) } catch let error as GraphQLError { - instrumentation.operationExecution( - processId: processId(), - threadId: threadId(), - started: executeStarted, - finished: instrumentation.now, - schema: schema, - document: documentAST, - rootValue: rootValue, - variableValues: variableValues, - operation: nil, - errors: [error], - result: nil - ) - return GraphQLResult(errors: [error]) } catch { return GraphQLResult(errors: [GraphQLError(error)]) @@ -264,7 +244,6 @@ func buildExecutionContext( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -318,7 +297,6 @@ func buildExecutionContext( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, fragments: fragments, rootValue: rootValue, @@ -641,8 +619,6 @@ 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 = await resolveOrError( @@ -653,17 +629,6 @@ public func resolveField( info: info ) -// exeContext.instrumentation.fieldResolution( -// processId: processId(), -// threadId: threadId(), -// started: resolveFieldStarted, -// finished: exeContext.instrumentation.now, -// source: source, -// args: args, -// info: info, -// result: result -// ) - return try await completeValueCatchingError( exeContext: exeContext, returnType: returnType, diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 24f26eb4..36bc3456 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -67,8 +67,6 @@ public struct SubscriptionResult { /// - 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 @@ -93,7 +91,6 @@ public func graphql( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, @@ -103,9 +100,8 @@ public func graphql( operationName: String? = nil ) async throws -> GraphQLResult { let source = Source(body: request, name: "GraphQL 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 @@ -119,7 +115,6 @@ public func graphql( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -135,8 +130,6 @@ public func graphql( /// - 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 @@ -160,7 +153,6 @@ public func graphql( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, queryRetrieval: Retrieval, queryId: Retrieval.Id, rootValue: Any = (), @@ -180,7 +172,6 @@ public func graphql( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -202,8 +193,6 @@ public func graphql( /// - 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 @@ -232,7 +221,6 @@ public func graphqlSubscribe( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - instrumentation: Instrumentation = NoOpInstrumentation, validationRules: [(ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, @@ -242,9 +230,8 @@ public func graphqlSubscribe( operationName: String? = nil ) async throws -> SubscriptionResult { 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 @@ -258,7 +245,6 @@ public func graphqlSubscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, 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 f58ce3f5..00000000 --- a/Sources/GraphQL/Instrumentation/DispatchQueueInstrumentationWrapper.swift +++ /dev/null @@ -1,136 +0,0 @@ -import Dispatch - -/// 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, - 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, - variableValues: variableValues, - operation: operation, - errors: errors, - result: result - ) - } - } - - public func fieldResolution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Any, - args: Map, - info: GraphQLResolveInfo, - result: Result - ) { - dispatchQueue.async(group: dispatchGroup) { - self.instrumentation.fieldResolution( - processId: processId, - threadId: threadId, - started: started, - finished: finished, - source: source, - args: args, - info: info, - result: result - ) - } - } -} diff --git a/Sources/GraphQL/Instrumentation/Instrumentation.swift b/Sources/GraphQL/Instrumentation/Instrumentation.swift deleted file mode 100644 index 90fd04b0..00000000 --- a/Sources/GraphQL/Instrumentation/Instrumentation.swift +++ /dev/null @@ -1,121 +0,0 @@ -import Dispatch -import Foundation - -/// 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, - variableValues: [String: Map], - operation: OperationDefinition?, - errors: [GraphQLError], - result: Map - ) - - func fieldResolution( - processId: Int, - threadId: Int, - started: DispatchTime, - finished: DispatchTime, - source: Any, - args: Map, - info: GraphQLResolveInfo, - result: Result - ) -} - -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, - variableValues _: [String: Map], - operation _: OperationDefinition?, - errors _: [GraphQLError], - result _: Map - ) {} - - public func fieldResolution( - processId _: Int, - threadId _: Int, - started _: DispatchTime, - finished _: DispatchTime, - source _: Any, - args _: Map, - info _: GraphQLResolveInfo, - result _: Result - ) {} -} 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/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index ac9f44db..82f7b842 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -24,7 +24,6 @@ func subscribe( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -36,7 +35,6 @@ func subscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -63,7 +61,6 @@ func subscribe( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: eventPayload, @@ -120,7 +117,6 @@ func createSourceEventStream( queryStrategy: QueryFieldExecutionStrategy, mutationStrategy: MutationFieldExecutionStrategy, subscriptionStrategy: SubscriptionFieldExecutionStrategy, - instrumentation: Instrumentation, schema: GraphQLSchema, documentAST: Document, rootValue: Any, @@ -128,8 +124,6 @@ func createSourceEventStream( variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> SourceEventStreamResult { - let executeStarted = instrumentation.now - do { // If a valid context cannot be created due to incorrect arguments, // this will throw an error. @@ -137,7 +131,6 @@ func createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, - instrumentation: instrumentation, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -147,20 +140,6 @@ func createSourceEventStream( ) 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, - variableValues: variableValues, - operation: nil, - errors: [error], - result: nil - ) - return SourceEventStreamResult(errors: [error]) } catch { return SourceEventStreamResult(errors: [GraphQLError(error)]) diff --git a/Sources/GraphQL/Validation/Validate.swift b/Sources/GraphQL/Validation/Validate.swift index 362d4ad4..dfa5f651 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -4,17 +4,15 @@ /// 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: []) + return validate(schema: schema, ast: ast, rules: []) } /** @@ -31,24 +29,13 @@ public func validate( * GraphQLErrors, or Arrays of GraphQLErrors when invalid. */ public func validate( - instrumentation: Instrumentation = NoOpInstrumentation, schema: GraphQLSchema, ast: Document, rules: [(ValidationContext) -> Visitor] ) -> [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 } diff --git a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift b/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift deleted file mode 100644 index 3583aa7f..00000000 --- a/Tests/GraphQLTests/InstrumentationTests/InstrumentationTests.swift +++ /dev/null @@ -1,194 +0,0 @@ -import Dispatch -import Foundation -import GraphQL -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, - 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, - info _: GraphQLResolveInfo, - result _: Result - ) { - 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/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 53d4bab1..9a49c80c 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -190,7 +190,6 @@ func createSubscription( queryStrategy: SerialFieldExecutionStrategy(), mutationStrategy: SerialFieldExecutionStrategy(), subscriptionStrategy: SerialFieldExecutionStrategy(), - instrumentation: NoOpInstrumentation, schema: schema, request: query, rootValue: (), From 1bc642c06f9c7447e2f5c31ed700c7af901ca8c2 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 26 Jun 2025 22:44:25 -0600 Subject: [PATCH 05/20] test: Fixes race condition in test This resolves the race condition caused by the inbox counts and the event delivery. If event delivery happens before the subsequent publish increments the inbox counts, then the counts will be lower than expected. Resolved by just not asking for inbox counts, since they aren't relevant to the test. --- .../SubscriptionTests/SubscriptionTests.swift | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index 8711991b..e34afe49 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -711,10 +711,6 @@ class SubscriptionTests: XCTestCase { from subject } - inbox { - unread - total - } } } """) @@ -734,6 +730,8 @@ class SubscriptionTests: XCTestCase { )) let result1 = try await iterator.next() + let result2 = try await iterator.next() + XCTAssertEqual( result1, GraphQLResult( @@ -742,15 +740,9 @@ class SubscriptionTests: XCTestCase { "from": "yuzhi@graphql.org", "subject": "Alright", ], - "inbox": [ - "unread": 2, - "total": 3, - ], ]] ) ) - - let result2 = try await iterator.next() XCTAssertEqual( result2, GraphQLResult( @@ -759,10 +751,6 @@ class SubscriptionTests: XCTestCase { "from": "yuzhi@graphql.org", "subject": "Message 2", ], - "inbox": [ - "unread": 2, - "total": 3, - ], ]] ) ) From d25254b60a31437b1be017e3e322480f71d22321 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Tue, 1 Jul 2025 17:10:56 -0600 Subject: [PATCH 06/20] test: Moves test off of global dispatch queue This was causing test hangs on macOS --- .../FieldExecutionStrategyTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift index cf8e09e1..3caee2e0 100644 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift @@ -2,6 +2,8 @@ import Dispatch @testable import GraphQL import XCTest +let queue = DispatchQueue(label: "testQueue") + class FieldExecutionStrategyTests: XCTestCase { enum StrategyError: Error { case exampleError(msg: String) @@ -24,7 +26,7 @@ class FieldExecutionStrategyTests: XCTestCase { let group = DispatchGroup() group.enter() - DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) { + queue.asyncAfter(wallDeadline: .now() + 0.1) { group.leave() } @@ -41,7 +43,7 @@ class FieldExecutionStrategyTests: XCTestCase { let g = DispatchGroup() g.enter() - DispatchQueue.global().asyncAfter(wallDeadline: .now() + 0.1) { + queue.asyncAfter(wallDeadline: .now() + 0.1) { g.leave() } From 631650a9ea3cb6a9f749e3192a7e06ba8ff00586 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 4 Jul 2025 23:39:59 -0600 Subject: [PATCH 07/20] feat!: Switches SubscriptionResult with Result --- MIGRATION.md | 4 + Sources/GraphQL/GraphQL.swift | 15 +-- Sources/GraphQL/Subscription/Subscribe.swift | 103 +++++++----------- .../SubscriptionSchema.swift | 7 +- .../SubscriptionTests/SubscriptionTests.swift | 31 +++--- 5 files changed, 68 insertions(+), 92 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 325fc0cd..73fa8534 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,6 +20,10 @@ This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. 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 a true Swift `Result` type. + ### 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. diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 36bc3456..525b1970 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -42,16 +42,11 @@ 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: AsyncThrowingStream? +/// A collection of GraphQL errors. Enables returning multiple errors from Result types. +public struct GraphQLErrors: Error, Sendable { public let errors: [GraphQLError] - public init( - stream: AsyncThrowingStream? = nil, - errors: [GraphQLError] = [] - ) { - self.stream = stream + public init(_ errors: [GraphQLError]) { self.errors = errors } } @@ -228,7 +223,7 @@ public func graphqlSubscribe( context: Any = (), variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> SubscriptionResult { +) async throws -> Result, GraphQLErrors> { let source = Source(body: request, name: "GraphQL Subscription request") let documentAST = try parse(source: source) let validationErrors = validate( @@ -238,7 +233,7 @@ public func graphqlSubscribe( ) guard validationErrors.isEmpty else { - return SubscriptionResult(errors: validationErrors) + return .failure(.init(validationErrors)) } return try await subscribe( diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 82f7b842..dd1c6d5f 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -3,22 +3,16 @@ 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, @@ -30,7 +24,7 @@ func subscribe( context: Any, variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> SubscriptionResult { +) async throws -> Result, GraphQLErrors> { let sourceResult = try await createSourceEventStream( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, @@ -43,7 +37,7 @@ func subscribe( operationName: operationName ) - if let sourceStream = sourceResult.stream { + return sourceResult.map { sourceStream in // We must create a new AsyncSequence because AsyncSequence.map requires a concrete type // (which we cannot know), // and we need the result to be a concrete type. @@ -80,9 +74,7 @@ func subscribe( task.cancel() } } - return SubscriptionResult(stream: subscriptionStream, errors: sourceResult.errors) - } else { - return SubscriptionResult(errors: sourceResult.errors) + return subscriptionStream } } @@ -90,20 +82,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. @@ -123,32 +111,37 @@ func createSourceEventStream( context: Any, variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> SourceEventStreamResult { +) async throws -> Result { + // 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, + 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, - schema: schema, - documentAST: documentAST, - rootValue: rootValue, - context: context, - rawVariableValues: variableValues, - operationName: operationName - ) return try await executeSubscription(context: exeContext) } catch let error as GraphQLError { - return 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 SourceEventStreamResult(errors: [GraphQLError(error)]) + // Otherwise treat the error as a system-class error and re-throw it. + throw error } } func executeSubscription( context: ExecutionContext -) async throws -> SourceEventStreamResult { +) async throws -> Result { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) var inputFields: OrderedDictionary = [:] @@ -238,35 +231,21 @@ func executeSubscription( resolved = success } if !context.errors.isEmpty { - return SourceEventStreamResult(errors: context.errors) + return .failure(.init(context.errors)) } else if let error = resolved as? GraphQLError { - return SourceEventStreamResult(errors: [error]) + return .failure(.init([error])) } else if let stream = resolved as? any AsyncSequence { - return SourceEventStreamResult(stream: stream) + return .success(stream) } else if resolved == nil { - return SourceEventStreamResult(errors: [ + return .failure(.init([ GraphQLError(message: "Resolved subscription was nil"), - ]) + ])) } else { let resolvedObj = resolved as AnyObject - return SourceEventStreamResult(errors: [ + return .failure(.init([ GraphQLError( message: "Subscription field resolver must return an AsyncSequence. 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: (any AsyncSequence)? - public let errors: [GraphQLError] - - public init(stream: (any AsyncSequence)? = nil, errors: [GraphQLError] = []) { - self.stream = stream - self.errors = errors + ])) } } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 9a49c80c..1670347d 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -197,10 +197,5 @@ func createSubscription( variableValues: variableValues, operationName: nil ) - - 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 e34afe49..660737ff 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -30,10 +30,7 @@ class SubscriptionTests: XCTestCase { schema: schema, request: query ) - guard let stream = subscriptionResult.stream else { - XCTFail(subscriptionResult.errors.description) - return - } + let stream = try subscriptionResult.get() var iterator = stream.makeAsyncIterator() db.trigger(email: Email( @@ -241,15 +238,19 @@ class SubscriptionTests: XCTestCase { """) XCTFail("Error should have been thrown") } catch { - guard let graphQLError = error as? GraphQLError else { - XCTFail("Error was not of type GraphQLError") + 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)]) } } @@ -284,13 +285,15 @@ class SubscriptionTests: XCTestCase { """) XCTFail("Error should have been thrown") } catch { - guard let graphQLError = error as? GraphQLError else { + guard let graphQLErrors = error as? GraphQLErrors else { XCTFail("Error was not of type GraphQLError") return } XCTAssertEqual( - graphQLError.message, - "Subscription field resolver must return an AsyncSequence. Received: 'test'" + graphQLErrors.errors, + [GraphQLError( + message: "Subscription field resolver must return an AsyncSequence. Received: 'test'" + )] ) } } @@ -310,11 +313,11 @@ class SubscriptionTests: XCTestCase { """) XCTFail("Error should have been thrown") } catch { - guard let graphQLError = error as? GraphQLError else { + 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")]) } } From 34268d3ccd53f92d77ac54583a34172e001ae7a8 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sat, 5 Jul 2025 00:15:08 -0600 Subject: [PATCH 08/20] docs: Updates subscription docs in README --- README.md | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 8f94cb5b..284de788 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,9 @@ 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( @@ -71,16 +68,16 @@ let schema = try GraphQLSchema( fields: [ "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 + 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 } } } @@ -96,7 +93,8 @@ To execute a subscription use the `graphqlSubscribe` function: let subscriptionResult = try await graphqlSubscribe( schema: schema, ) -for try await result in concurrentStream.stream { +let stream = subscriptionResult.get() +for try await result in stream { print(result) } ``` From e1c66d92810dc24bc5ff712791c849c3874db425 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 14 Jul 2025 22:56:37 -0600 Subject: [PATCH 09/20] fix: Avoids unstructured task --- Sources/GraphQL/Subscription/Subscribe.swift | 55 ++++++------------- .../SubscriptionSchema.swift | 12 ++-- .../SubscriptionTests/SubscriptionTests.swift | 32 +---------- 3 files changed, 23 insertions(+), 76 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index dd1c6d5f..72222bd7 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -38,43 +38,24 @@ func subscribe( ) return sourceResult.map { sourceStream in - // We must create a new AsyncSequence because AsyncSequence.map requires a concrete type - // (which we cannot know), - // and we need the result to be a concrete type. - let subscriptionStream = AsyncThrowingStream { continuation in - let task = Task { - do { - for try await eventPayload in sourceStream { - // 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. - let newEvent = try await execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, - schema: schema, - documentAST: documentAST, - rootValue: eventPayload, - context: context, - variableValues: variableValues, - operationName: operationName - ) - continuation.yield(newEvent) - } - continuation.finish() - } catch { - continuation.finish(throwing: error) - } - } - - continuation.onTermination = { @Sendable reason in - task.cancel() + 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 try await execute( + queryStrategy: queryStrategy, + mutationStrategy: mutationStrategy, + subscriptionStrategy: subscriptionStrategy, + schema: schema, + documentAST: documentAST, + rootValue: eventPayload, + context: context, + variableValues: variableValues, + operationName: operationName + ) } - return subscriptionStream } } @@ -111,7 +92,7 @@ func createSourceEventStream( context: Any, variableValues: [String: Map] = [:], operationName: String? = nil -) async throws -> Result { +) async throws -> Result { // If a valid context cannot be created due to incorrect arguments, // this will throw an error. let exeContext = try buildExecutionContext( @@ -141,7 +122,7 @@ func createSourceEventStream( func executeSubscription( context: ExecutionContext -) async throws -> Result { +) async throws -> Result { // Get the first node let type = try getOperationRootType(schema: context.schema, operation: context.operation) var inputFields: OrderedDictionary = [:] diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 1670347d..308199cb 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -90,7 +90,7 @@ let EmailQueryType = try! GraphQLObjectType( class EmailDb { var emails: [Email] - let publisher: SimplePubSub + let publisher: SimplePubSub init() { emails = [ @@ -101,7 +101,7 @@ class EmailDb { unread: false ), ] - publisher = SimplePubSub() + publisher = SimplePubSub() } /// Adds a new email to the database and triggers all observers @@ -129,12 +129,8 @@ class EmailDb { }, subscribe: { _, args, _, _ throws -> Any? in let priority = args["priority"].int ?? 0 - let filtered = self.publisher.subscribe().filter { emailAny throws in - if let email = emailAny as? Email { - return email.priority >= priority - } else { - return true - } + let filtered = self.publisher.subscribe().filter { email throws in + return email.priority >= priority } return filtered } diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index 660737ff..36eb9f98 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -857,35 +857,5 @@ class SubscriptionTests: XCTestCase { // Handled by AsyncThrowingStream /// Test incorrect emitted type errors - func testErrorWrongEmitType() async throws { - let db = EmailDb() - let stream = try await db.subscription(query: """ - subscription ($priority: Int = 0) { - importantEmail(priority: $priority) { - email { - from - subject - } - inbox { - unread - total - } - } - } - """) - var iterator = stream.makeAsyncIterator() - - db.publisher.emit(event: "String instead of email") - - let result = try await iterator.next() - XCTAssertEqual( - result, - GraphQLResult( - data: ["importantEmail": nil], - errors: [ - GraphQLError(message: "String is not Email"), - ] - ) - ) - } + // Handled by strongly typed PubSub } From 0d90d04a498bb26af2e714756e4f9e3da805c80e Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Mon, 14 Jul 2025 23:34:26 -0600 Subject: [PATCH 10/20] chore: swiftformat updates --- Sources/GraphQL/Subscription/Subscribe.swift | 3 ++- Sources/GraphQL/Type/Introspection.swift | 4 +++- .../SubscriptionTests/SubscriptionTests.swift | 2 +- .../UtilitiesTests/BuildASTSchemaTests.swift | 4 ++-- .../UtilitiesTests/PrintSchemaTests.swift | 4 +++- .../ValidationTests/ExampleSchema.swift | 12 +++++++++--- .../NoDeprecatedCustomRuleTests.swift | 18 ++++++++++++------ 7 files changed, 32 insertions(+), 15 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 72222bd7..7c88f2d1 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -39,7 +39,8 @@ func subscribe( 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. + // 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 diff --git a/Sources/GraphQL/Type/Introspection.swift b/Sources/GraphQL/Type/Introspection.swift index 89c2cc2f..bac127c2 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -83,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: [ diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index 36eb9f98..54991670 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -194,7 +194,7 @@ class SubscriptionTests: XCTestCase { ] ) ) - let _ = try await createSubscription(schema: schema, query: """ + _ = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { diff --git a/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift b/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift index 91429598..e827cf0f 100644 --- a/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift +++ b/Tests/GraphQLTests/UtilitiesTests/BuildASTSchemaTests.swift @@ -988,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/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 d0571f94..f874014b 100644 --- a/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift +++ b/Tests/GraphQLTests/ValidationTests/ExampleSchema.swift @@ -288,7 +288,9 @@ 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 }, @@ -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 }, @@ -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: [ From 2cbc8b92740aa4268a84cc163393fc4a9a69a93e Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Thu, 31 Jul 2025 23:34:51 -0600 Subject: [PATCH 11/20] feat!: Deletes deprecated Node.set func --- MIGRATION.md | 4 ++++ Sources/GraphQL/Language/AST.swift | 5 ----- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 73fa8534..1183c951 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -28,6 +28,10 @@ The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns a 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 diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 97274af3..3d0e7c7d 100644 --- a/Sources/GraphQL/Language/AST.swift +++ b/Sources/GraphQL/Language/AST.swift @@ -213,11 +213,6 @@ 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) { // This should be overridden by each type on which it should do something } From a34843424d32704ea8ddd288b376ebe3e8967c3d Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Fri, 1 Aug 2025 00:22:18 -0600 Subject: [PATCH 12/20] feat: Makes FieldExecutionStrategy sendable --- Sources/GraphQL/Execution/Execute.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 403b65ab..7c7e2e77 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -82,7 +82,7 @@ public final class ExecutionContext { } } -public protocol FieldExecutionStrategy { +public protocol FieldExecutionStrategy: Sendable { func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, From bc807e3bff1edceaa4bbb9b8ad2d6c6cc42aee69 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sat, 2 Aug 2025 00:31:53 -0600 Subject: [PATCH 13/20] feat!: Enable strict concurrency --- MIGRATION.md | 6 +- Package.swift | 3 +- Sources/GraphQL/Error/GraphQLError.swift | 4 +- Sources/GraphQL/Execution/Execute.swift | 95 ++-- Sources/GraphQL/GraphQL.swift | 16 +- Sources/GraphQL/Language/AST.swift | 453 +++++++++++------- Sources/GraphQL/Language/Kinds.swift | 2 +- Sources/GraphQL/Language/Location.swift | 2 +- Sources/GraphQL/Language/Source.swift | 2 +- Sources/GraphQL/Language/Visitor.swift | 12 +- Sources/GraphQL/Map/AnyCoder.swift | 10 +- Sources/GraphQL/Map/GraphQLJSONEncoder.swift | 10 +- Sources/GraphQL/Map/MapCoder.swift | 14 +- Sources/GraphQL/Subscription/Subscribe.swift | 17 +- Sources/GraphQL/SwiftUtilities/Mirror.swift | 13 +- Sources/GraphQL/Type/Definition.swift | 88 ++-- Sources/GraphQL/Type/Directives.swift | 4 +- Sources/GraphQL/Type/Introspection.swift | 5 +- Sources/GraphQL/Type/Schema.swift | 2 +- Sources/GraphQL/Utilities/ExtendSchema.swift | 2 +- Sources/GraphQL/Utilities/Keyable.swift | 2 +- .../GraphQL/Validation/SpecifiedRules.swift | 2 +- Sources/GraphQL/Validation/Validate.swift | 4 +- .../Validation/ValidationContext.swift | 4 +- .../LanguageTests/VisitorTests.swift | 32 +- .../StarWarsTests/StarWarsData.swift | 14 +- .../SubscriptionTests/SimplePubSub.swift | 2 +- .../SubscriptionSchema.swift | 18 +- .../SubscriptionTests/SubscriptionTests.swift | 113 +++-- .../ValidationTests/ValidationTests.swift | 4 +- 30 files changed, 543 insertions(+), 412 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index 1183c951..cbe1066e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -12,6 +12,10 @@ Also, all resolver closures must remove the `eventLoopGroup` argument, and all t 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). + ### `ConcurrentDispatchFieldExecutionStrategy` This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. @@ -22,7 +26,7 @@ The `EventStream` abstraction used to provide pre-concurrency subscription suppo ### SubscriptionResult removal -The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns a true Swift `Result` type. +The `SubscriptionResult` type was removed, and `graphqlSubscribe` now returns `Result, GraphQLErrors>`. ### Instrumentation removal diff --git a/Package.swift b/Package.swift index 9bf74c2f..bcc14305 100644 --- a/Package.swift +++ b/Package.swift @@ -25,5 +25,6 @@ let package = Package( .copy("LanguageTests/schema-kitchen-sink.graphql"), ] ), - ] + ], + swiftLanguageVersions: [.v5, .version("6")] ) 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 7c7e2e77..19c35748 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -27,14 +27,14 @@ import OrderedCollections * Namely, schema of the type system that is currently executing, * and the fragments defined in the query document */ -public final class ExecutionContext { +public final class ExecutionContext: @unchecked Sendable { let queryStrategy: QueryFieldExecutionStrategy let mutationStrategy: MutationFieldExecutionStrategy let subscriptionStrategy: SubscriptionFieldExecutionStrategy public let schema: GraphQLSchema public let fragments: [String: FragmentDefinition] - public let rootValue: Any - public let context: Any + public let rootValue: any Sendable + public let context: any Sendable public let operation: OperationDefinition public let variableValues: [String: Map] @@ -55,8 +55,8 @@ public final class ExecutionContext { subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, fragments: [String: FragmentDefinition], - rootValue: Any, - context: Any, + rootValue: any Sendable, + context: any Sendable, operation: OperationDefinition, variableValues: [String: Map], errors: [GraphQLError] @@ -86,10 +86,10 @@ public protocol FieldExecutionStrategy: Sendable { func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - sourceValue: Any, + sourceValue: any Sendable, path: IndexPath, fields: OrderedDictionary - ) async throws -> OrderedDictionary + ) async throws -> OrderedDictionary } public protocol MutationFieldExecutionStrategy: FieldExecutionStrategy {} @@ -107,11 +107,11 @@ public struct SerialFieldExecutionStrategy: QueryFieldExecutionStrategy, public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - sourceValue: Any, + sourceValue: any Sendable, path: IndexPath, fields: OrderedDictionary - ) async throws -> OrderedDictionary { - var results = OrderedDictionary() + ) async throws -> OrderedDictionary { + var results = OrderedDictionary() for field in fields { let fieldASTs = field.value let fieldPath = path.appending(field.key) @@ -138,13 +138,14 @@ public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, public func executeFields( exeContext: ExecutionContext, parentType: GraphQLObjectType, - sourceValue: Any, + sourceValue: any Sendable, path: IndexPath, fields: OrderedDictionary - ) async throws -> OrderedDictionary { - return try await withThrowingTaskGroup(of: (String, Any?).self) { group in + ) 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 } + var results: OrderedDictionary = fields + .mapValues { _ -> Any? in nil } for field in fields { group.addTask { let fieldASTs = field.value @@ -179,8 +180,8 @@ func execute( subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, + rootValue: any Sendable, + context: any Sendable, variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> GraphQLResult { @@ -246,8 +247,8 @@ func buildExecutionContext( subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, + rootValue: any Sendable, + context: any Sendable, rawVariableValues: [String: Map], operationName: String? ) throws -> ExecutionContext { @@ -313,8 +314,8 @@ func buildExecutionContext( func executeOperation( exeContext: ExecutionContext, operation: OperationDefinition, - rootValue: Any -) async throws -> OrderedDictionary { + rootValue: any Sendable +) async throws -> OrderedDictionary { let type = try getOperationRootType(schema: exeContext.schema, operation: operation) var inputFields: OrderedDictionary = [:] var visitedFragmentNames: [String: Bool] = [:] @@ -574,10 +575,10 @@ func getFieldEntryKey(node: Field) -> String { public func resolveField( exeContext: ExecutionContext, parentType: GraphQLObjectType, - source: Any, + source: any Sendable, fieldASTs: [Field], path: IndexPath -) async throws -> Any? { +) async throws -> (any Sendable)? { let fieldAST = fieldASTs[0] let fieldName = fieldAST.name.value @@ -643,11 +644,11 @@ 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, + context: any Sendable, info: GraphQLResolveInfo -) async -> Result { +) async -> Result<(any Sendable)?, Error> { do { let result = try await resolve(source, args, context, info) return .success(result) @@ -664,8 +665,8 @@ func completeValueCatchingError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result -) async throws -> Any? { + 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 { @@ -708,8 +709,8 @@ func completeValueWithLocatedError( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result -) async throws -> Any? { + result: Result<(any Sendable)?, Error> +) async throws -> (any Sendable)? { do { return try await completeValue( exeContext: exeContext, @@ -755,8 +756,8 @@ func completeValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Result -) async throws -> Any? { + result: Result<(any Sendable)?, Error> +) async throws -> (any Sendable)? { switch result { case let .failure(error): throw error @@ -846,9 +847,9 @@ func completeListValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Any -) async throws -> [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 " + @@ -858,9 +859,9 @@ func completeListValue( let itemType = returnType.ofType - return try await withThrowingTaskGroup(of: (Int, Any?).self) { group in + 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?] = result.map { _ in nil } + 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, @@ -889,7 +890,7 @@ func completeListValue( * 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 } @@ -910,8 +911,8 @@ func completeAbstractValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Any -) async throws -> Any? { + result: any Sendable +) async throws -> (any Sendable)? { var resolveRes = try returnType.resolveType?(result, info) .typeResolveResult @@ -976,8 +977,8 @@ func completeObjectValue( fieldASTs: [Field], info: GraphQLResolveInfo, path: IndexPath, - result: Any -) async throws -> Any? { + 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. @@ -1023,7 +1024,7 @@ func completeObjectValue( * isTypeOf for the object being coerced, returning the first type that matches. */ func defaultResolveType( - value: Any, + value: any Sendable, info: GraphQLResolveInfo, abstractType: GraphQLAbstractType ) throws -> TypeResolveResult? { @@ -1045,11 +1046,11 @@ func defaultResolveType( * and returns it as the result. */ func defaultResolve( - source: Any, + source: any Sendable, args _: Map, - context _: Any, + context _: any Sendable, info: GraphQLResolveInfo -) async throws -> Any? { +) async throws -> (any Sendable)? { guard let source = unwrap(source) else { return nil } @@ -1058,11 +1059,11 @@ func defaultResolve( let value = subscriptable[info.fieldName] return value } - if let subscriptable = source as? [String: Any] { + if let subscriptable = source as? [String: any Sendable] { let value = subscriptable[info.fieldName] return value } - if let subscriptable = source as? OrderedDictionary { + if let subscriptable = source as? OrderedDictionary { let value = subscriptable[info.fieldName] return value } diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 525b1970..1020f8ae 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -86,11 +86,11 @@ public func graphql( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - validationRules: [(ValidationContext) -> Visitor] = [], + validationRules: [@Sendable (ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, - rootValue: Any = (), - context: Any = (), + rootValue: (any Sendable) = (), + context: (any Sendable) = (), variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> GraphQLResult { @@ -150,8 +150,8 @@ public func graphql( subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), queryRetrieval: Retrieval, queryId: Retrieval.Id, - rootValue: Any = (), - context: Any = (), + rootValue: (any Sendable) = (), + context: (any Sendable) = (), variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> GraphQLResult { @@ -216,11 +216,11 @@ public func graphqlSubscribe( queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(), mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(), subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(), - validationRules: [(ValidationContext) -> Visitor] = [], + validationRules: [@Sendable (ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, - rootValue: Any = (), - context: Any = (), + rootValue: (any Sendable) = (), + context: (any Sendable) = (), variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> Result, GraphQLErrors> { diff --git a/Sources/GraphQL/Language/AST.swift b/Sources/GraphQL/Language/AST.swift index 3d0e7c7d..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,8 +214,9 @@ public extension Node { return nil } - 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 } } @@ -256,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 @@ -273,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] @@ -295,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 } } @@ -355,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 @@ -365,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, @@ -407,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": @@ -417,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 } } } @@ -464,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 @@ -504,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 } } @@ -569,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 @@ -588,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 } } @@ -616,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 @@ -635,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": @@ -645,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 } } } @@ -700,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? @@ -748,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 } } @@ -809,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 @@ -832,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 } } @@ -870,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 @@ -896,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 } } @@ -946,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? @@ -964,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) } @@ -983,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 } } @@ -1029,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, @@ -1066,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": @@ -1076,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 } } } @@ -1178,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 @@ -1195,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 @@ -1212,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 @@ -1231,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 @@ -1248,7 +1317,7 @@ extension BooleanValue: Equatable { } } -public final class NullValue { +public struct NullValue { public let kind: Kind = .nullValue public let loc: Location? @@ -1263,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 @@ -1280,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] @@ -1299,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 } } @@ -1334,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] @@ -1353,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 } } @@ -1378,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 @@ -1401,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 } } @@ -1435,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 @@ -1458,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 } } @@ -1518,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 @@ -1537,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 } } @@ -1562,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 @@ -1581,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 } } @@ -1610,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 @@ -1629,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 } } @@ -1716,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? @@ -1757,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 @@ -1830,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? @@ -1871,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? @@ -1924,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? @@ -1977,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? @@ -2046,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? @@ -2098,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? @@ -2145,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? @@ -2192,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? @@ -2233,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? @@ -2291,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 @@ -2316,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 @@ -2337,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 @@ -2365,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 @@ -2392,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 @@ -2417,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 @@ -2442,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 @@ -2470,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/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 debf273d..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 @@ -839,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.") } @@ -3062,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." @@ -3255,11 +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. -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 cb6029a7..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 @@ -560,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.") } @@ -1296,11 +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. -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 cc334dda..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 @@ -847,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.") } @@ -1094,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. @@ -3070,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." @@ -3263,11 +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. -private var _iso8601Formatter: ISO8601DateFormatter = { +private func _iso8601Formatter() -> ISO8601DateFormatter { let formatter = ISO8601DateFormatter() formatter.formatOptions = .withInternetDateTime return formatter -}() +} //===----------------------------------------------------------------------===// // Error Utilities diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 7c88f2d1..61dae71e 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -20,8 +20,8 @@ func subscribe( subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, + rootValue: any Sendable, + context: any Sendable, variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> Result, GraphQLErrors> { @@ -45,13 +45,16 @@ func subscribe( guard let eventPayload = try await iterator.next() else { return nil } + // 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( queryStrategy: queryStrategy, mutationStrategy: mutationStrategy, subscriptionStrategy: subscriptionStrategy, schema: schema, documentAST: documentAST, - rootValue: eventPayload, + rootValue: rootValue, context: context, variableValues: variableValues, operationName: operationName @@ -89,8 +92,8 @@ func createSourceEventStream( subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, - rootValue: Any, - context: Any, + rootValue: any Sendable, + context: any Sendable, variableValues: [String: Map] = [:], operationName: String? = nil ) async throws -> Result { @@ -217,7 +220,9 @@ func executeSubscription( } else if let error = resolved as? GraphQLError { return .failure(.init([error])) } else if let stream = resolved as? any AsyncSequence { - return .success(stream) + // 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"), 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 409d8344..20b15488 100644 --- a/Sources/GraphQL/Type/Definition.swift +++ b/Sources/GraphQL/Type/Definition.swift @@ -4,7 +4,7 @@ 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 {} @@ -157,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? @@ -165,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 { @@ -206,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) } @@ -272,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? @@ -406,36 +408,36 @@ extension String: TypeResolveResultRepresentable { } } -public enum TypeResolveResult { +public enum TypeResolveResult: Sendable { case type(GraphQLObjectType) case name(String) } -public typealias GraphQLTypeResolve = ( - _ value: Any, +public typealias GraphQLTypeResolve = @Sendable ( + _ value: any Sendable, _ info: GraphQLResolveInfo ) throws -> TypeResolveResultRepresentable -public typealias GraphQLIsTypeOf = ( - _ source: Any, +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, + _ context: any Sendable, _ info: GraphQLResolveInfo -) async throws -> Any? +) 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 @@ -443,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? @@ -517,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? @@ -572,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? @@ -594,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? @@ -651,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? @@ -754,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? @@ -853,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] @@ -1005,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? @@ -1050,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] @@ -1143,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? @@ -1167,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 bac127c2..185aba08 100644 --- a/Sources/GraphQL/Type/Introspection.swift +++ b/Sources/GraphQL/Type/Introspection.swift @@ -202,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( @@ -427,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" diff --git a/Sources/GraphQL/Type/Schema.swift b/Sources/GraphQL/Type/Schema.swift index 3b06b455..4bad22b7 100644 --- a/Sources/GraphQL/Type/Schema.swift +++ b/Sources/GraphQL/Type/Schema.swift @@ -27,7 +27,7 @@ import OrderedCollections * ) * */ -public final class GraphQLSchema { +public final class GraphQLSchema: @unchecked Sendable { let description: String? let extensions: [GraphQLSchemaExtensions] let astNode: SchemaDefinition? 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/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 dfa5f651..6144e290 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -31,7 +31,7 @@ public func validate( public func validate( schema: GraphQLSchema, ast: Document, - rules: [(ValidationContext) -> Visitor] + rules: [@Sendable (ValidationContext) -> Visitor] ) -> [GraphQLError] { let typeInfo = TypeInfo(schema: schema) let rules = rules.isEmpty ? specifiedRules : rules @@ -69,7 +69,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/LanguageTests/VisitorTests.swift b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift index 2b78e00a..edc28c7e 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) } } 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/SubscriptionTests/SimplePubSub.swift b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift index fb198bfa..906e8c1e 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SimplePubSub.swift @@ -1,7 +1,7 @@ import GraphQL /// A very simple publish/subscriber used for testing -class SimplePubSub { +actor SimplePubSub { private var subscribers: [Subscriber] init() { diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 308199cb..0b65573c 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -18,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 } @@ -88,7 +88,7 @@ let EmailQueryType = try! GraphQLObjectType( // MARK: Test Helpers -class EmailDb { +actor EmailDb { var emails: [Email] let publisher: SimplePubSub @@ -105,13 +105,13 @@ class EmailDb { } /// 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. @@ -119,7 +119,7 @@ class EmailDb { return try emailSchemaWithResolvers( resolve: { emailAny, _, _, _ throws -> Any? in if let email = emailAny as? Email { - return EmailEvent( + return await EmailEvent( email: email, inbox: Inbox(emails: self.emails) ) @@ -129,7 +129,7 @@ class EmailDb { }, subscribe: { _, args, _, _ throws -> Any? in let priority = args["priority"].int ?? 0 - let filtered = self.publisher.subscribe().filter { email throws in + let filtered = await self.publisher.subscribe().filter { email throws in return email.priority >= priority } return filtered diff --git a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift index 54991670..5f9a8da9 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionTests.swift @@ -10,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) { @@ -33,13 +33,13 @@ class SubscriptionTests: XCTestCase { 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() + await db.stop() let result = try await iterator.next() XCTAssertEqual( result, @@ -75,7 +75,7 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, _ throws -> Any? in + resolve: { emailAny, _, _, _ throws in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" @@ -83,11 +83,11 @@ class SubscriptionTests: XCTestCase { } return EmailEvent( email: email, - inbox: Inbox(emails: db.emails) + inbox: Inbox(emails: await db.emails) ) }, - subscribe: { _, _, _, _ throws -> Any? in - db.publisher.subscribe() + subscribe: { _, _, _, _ throws in + await db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( @@ -97,7 +97,7 @@ class SubscriptionTests: XCTestCase { type: GraphQLInt ), ], - resolve: { emailAny, _, _, _ throws -> Any? in + resolve: { emailAny, _, _, _ throws in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" @@ -105,11 +105,11 @@ class SubscriptionTests: XCTestCase { } return EmailEvent( email: email, - inbox: Inbox(emails: db.emails) + inbox: Inbox(emails: await db.emails) ) }, - subscribe: { _, _, _, _ throws -> Any? in - db.publisher.subscribe() + subscribe: { _, _, _, _ throws in + await db.publisher.subscribe() } ), ] @@ -131,7 +131,7 @@ class SubscriptionTests: XCTestCase { """) var iterator = stream.makeAsyncIterator() - db.trigger(email: Email( + await db.trigger(email: Email( from: "yuzhi@graphql.org", subject: "Alright", message: "Tests are good", @@ -163,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, @@ -173,28 +183,28 @@ class SubscriptionTests: XCTestCase { fields: [ "importantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, _ throws -> Any? in + resolve: { _, _, _, _ in nil }, - subscribe: { _, _, _, _ throws -> Any? in - didResolveImportantEmail = true - return db.publisher.subscribe() + subscribe: { _, _, _, _ in + await resolveChecker.resolveImportantEmail() + return await db.publisher.subscribe() } ), "notImportantEmail": GraphQLField( type: EmailEventType, - resolve: { _, _, _, _ throws -> Any? in + resolve: { _, _, _, _ in nil }, - subscribe: { _, _, _, _ throws -> Any? in - didResolveNonImportantEmail = true - return db.publisher.subscribe() + subscribe: { _, _, _, _ in + await resolveChecker.resolveNonImportantEmail() + return await db.publisher.subscribe() } ), ] ) ) - _ = try await createSubscription(schema: schema, query: """ + let subscriptionResult = try await createSubscription(schema: schema, query: """ subscription { importantEmail { email { @@ -208,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) } @@ -266,10 +281,10 @@ class SubscriptionTests: XCTestCase { /// 'throws an error if subscribe does not return an iterator' func testErrorIfSubscribeIsntIterator() async throws { let schema = try emailSchemaWithResolvers( - resolve: { _, _, _, _ throws -> Any? in + resolve: { _, _, _, _ throws in nil }, - subscribe: { _, _, _, _ throws -> Any? in + subscribe: { _, _, _, _ throws in "test" } ) @@ -323,21 +338,21 @@ class SubscriptionTests: XCTestCase { // Throwing an error try await verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _ throws -> Any? in + subscribe: { _, _, _, _ throws in throw GraphQLError(message: "test error") } )) // Resolving to an error try await verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _ throws -> Any? in + subscribe: { _, _, _, _ throws in GraphQLError(message: "test error") } )) // Rejecting with an error try await verifyError(schema: emailSchemaWithResolvers( - subscribe: { _, _, _, _ throws -> Any? in + subscribe: { _, _, _, _ throws in GraphQLError(message: "test error") } )) @@ -405,13 +420,13 @@ class SubscriptionTests: XCTestCase { """) 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() XCTAssertEqual( @@ -467,7 +482,7 @@ class SubscriptionTests: XCTestCase { 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", @@ -514,7 +529,7 @@ class SubscriptionTests: XCTestCase { 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", @@ -538,7 +553,7 @@ 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", @@ -584,7 +599,7 @@ class SubscriptionTests: XCTestCase { 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", @@ -609,7 +624,7 @@ class SubscriptionTests: XCTestCase { XCTAssertEqual(results, expected) // Low priority email shouldn't trigger an event - db.trigger(email: Email( + await db.trigger(email: Email( from: "hyo@graphql.org", subject: "Not Important", message: "Ignore this email", @@ -619,7 +634,7 @@ class SubscriptionTests: XCTestCase { XCTAssertEqual(results, expected) // Higher priority one should trigger again - db.trigger(email: Email( + await db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", @@ -665,7 +680,7 @@ class SubscriptionTests: XCTestCase { 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", @@ -688,10 +703,10 @@ class SubscriptionTests: XCTestCase { try await results.append(iterator.next()) XCTAssertEqual(results, expected) - db.stop() + await db.stop() // This should not trigger an event. - db.trigger(email: Email( + await db.trigger(email: Email( from: "hyo@graphql.org", subject: "Tools", message: "I <3 making things", @@ -719,13 +734,13 @@ class SubscriptionTests: XCTestCase { """) 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", @@ -764,7 +779,7 @@ class SubscriptionTests: XCTestCase { let db = EmailDb() let schema = try emailSchemaWithResolvers( - resolve: { emailAny, _, _, _ throws -> Any? in + resolve: { emailAny, _, _, _ throws in guard let email = emailAny as? Email else { throw GraphQLError( message: "Source is not Email type: \(type(of: emailAny))" @@ -775,11 +790,11 @@ class SubscriptionTests: XCTestCase { } return EmailEvent( email: email, - inbox: Inbox(emails: db.emails) + inbox: Inbox(emails: await db.emails) ) }, - subscribe: { _, _, _, _ throws -> Any? in - db.publisher.subscribe() + subscribe: { _, _, _, _ throws in + await db.publisher.subscribe() } ) @@ -796,7 +811,7 @@ class SubscriptionTests: XCTestCase { 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", @@ -815,7 +830,7 @@ class SubscriptionTests: XCTestCase { XCTAssertEqual(results, expected) // 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", @@ -834,7 +849,7 @@ class SubscriptionTests: XCTestCase { // 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", diff --git a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift index 91f26c6f..15d4e5ef 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! @@ -131,7 +131,7 @@ class ValidationTestCase: XCTestCase { } class SDLValidationTestCase: XCTestCase { - typealias Rule = (SDLValidationContext) -> Visitor + typealias Rule = @Sendable (SDLValidationContext) -> Visitor var rule: Rule! From 5c1e0be9e9e80fb459cd2347c4bc3bc92ffa685b Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 18:37:25 -0600 Subject: [PATCH 14/20] feat!: Improves thread safety of unchecked Sendables --- Sources/GraphQL/Execution/Execute.swift | 27 +++-- Sources/GraphQL/Type/Schema.swift | 131 +++++++++++++++--------- 2 files changed, 101 insertions(+), 57 deletions(-) diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index 19c35748..beba715e 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -38,15 +38,24 @@ public final class ExecutionContext: @unchecked 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( @@ -74,11 +83,7 @@ public final class ExecutionContext: @unchecked Sendable { } public func append(error: GraphQLError) { - errorsSemaphore.wait() - defer { - errorsSemaphore.signal() - } - _errors.append(error) + errors.append(error) } } diff --git a/Sources/GraphQL/Type/Schema.swift b/Sources/GraphQL/Type/Schema.swift index 4bad22b7..969c41f5 100644 --- a/Sources/GraphQL/Type/Schema.swift +++ b/Sources/GraphQL/Type/Schema.swift @@ -1,3 +1,4 @@ +import Dispatch import OrderedCollections /** @@ -34,15 +35,53 @@ public final class GraphQLSchema: @unchecked Sendable { 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: @unchecked Sendable { 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: @unchecked Sendable { 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: @unchecked Sendable { 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: @unchecked Sendable { 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 From bf8b94267e533e816e272f9e963fe8e09feacbed Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 17:35:08 -0600 Subject: [PATCH 15/20] feat!: Hides execution strategies, applying spec specified ones --- MIGRATION.md | 5 +- Sources/GraphQL/Execution/Execute.swift | 27 +- Sources/GraphQL/GraphQL.swift | 27 -- Sources/GraphQL/Subscription/Subscribe.swift | 15 - .../FieldExecutionStrategyTests.swift | 297 ------------------ .../SubscriptionSchema.swift | 3 - 6 files changed, 8 insertions(+), 366 deletions(-) delete mode 100644 Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift diff --git a/MIGRATION.md b/MIGRATION.md index cbe1066e..f29cd72a 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -16,9 +16,10 @@ The documentation here will be very helpful in the conversion: https://www.swift 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). -### `ConcurrentDispatchFieldExecutionStrategy` +### `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). -This was changed to `ConcurrentFieldExecutionStrategy`, and takes no parameters. ### EventStream removal diff --git a/Sources/GraphQL/Execution/Execute.swift b/Sources/GraphQL/Execution/Execute.swift index beba715e..4f3dfda2 100644 --- a/Sources/GraphQL/Execution/Execute.swift +++ b/Sources/GraphQL/Execution/Execute.swift @@ -28,9 +28,10 @@ import OrderedCollections * and the fragments defined in the query document */ public final class ExecutionContext: @unchecked Sendable { - let queryStrategy: QueryFieldExecutionStrategy - let mutationStrategy: MutationFieldExecutionStrategy - let subscriptionStrategy: SubscriptionFieldExecutionStrategy + 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 Sendable @@ -59,9 +60,6 @@ public final class ExecutionContext: @unchecked Sendable { } init( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, fragments: [String: FragmentDefinition], rootValue: any Sendable, @@ -70,9 +68,6 @@ public final class ExecutionContext: @unchecked Sendable { variableValues: [String: Map], errors: [GraphQLError] ) { - self.queryStrategy = queryStrategy - self.mutationStrategy = mutationStrategy - self.subscriptionStrategy = subscriptionStrategy self.schema = schema self.fragments = fragments self.rootValue = rootValue @@ -179,10 +174,7 @@ public struct ConcurrentFieldExecutionStrategy: QueryFieldExecutionStrategy, * 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, +public func execute( schema: GraphQLSchema, documentAST: Document, rootValue: any Sendable, @@ -196,9 +188,6 @@ func execute( // 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, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -247,9 +236,6 @@ func execute( * Throws a GraphQLError if a valid execution context cannot be created. */ func buildExecutionContext( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, rootValue: any Sendable, @@ -300,9 +286,6 @@ func buildExecutionContext( ) return ExecutionContext( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, schema: schema, fragments: fragments, rootValue: rootValue, diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 1020f8ae..10041e57 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -59,9 +59,6 @@ public struct GraphQLErrors: Error, Sendable { /// 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 schema: The GraphQL type system to use when validating and executing a /// query. /// - parameter request: A GraphQL language formatted string representing the requested @@ -83,9 +80,6 @@ public struct GraphQLErrors: Error, Sendable { /// 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(), validationRules: [@Sendable (ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, @@ -107,9 +101,6 @@ public func graphql( } return try await execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -122,9 +113,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 queryRetrieval: The PersistedQueryRetrieval instance to use for looking up /// queries /// - parameter queryId: The id of the query to execute @@ -145,9 +133,6 @@ 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(), queryRetrieval: Retrieval, queryId: Retrieval.Id, rootValue: (any Sendable) = (), @@ -164,9 +149,6 @@ public func graphql( return GraphQLResult(errors: validationErrors) case let .result(schema, documentAST): return try await execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -185,9 +167,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 schema: The GraphQL type system to use when validating and executing a /// query. /// - parameter request: A GraphQL language formatted string representing the requested @@ -213,9 +192,6 @@ 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(), validationRules: [@Sendable (ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, @@ -237,9 +213,6 @@ public func graphqlSubscribe( } return try await subscribe( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, schema: schema, documentAST: documentAST, rootValue: rootValue, diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 61dae71e..5a82a1cc 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -15,9 +15,6 @@ import OrderedCollections * representing the response stream. */ func subscribe( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, rootValue: any Sendable, @@ -26,9 +23,6 @@ func subscribe( operationName: String? = nil ) async throws -> Result, GraphQLErrors> { let sourceResult = try await createSourceEventStream( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -49,9 +43,6 @@ func subscribe( // `marker protocol 'Sendable' cannot be used in a conditional cast` let rootValue = eventPayload as! (any Sendable) return try await execute( - queryStrategy: queryStrategy, - mutationStrategy: mutationStrategy, - subscriptionStrategy: subscriptionStrategy, schema: schema, documentAST: documentAST, rootValue: rootValue, @@ -87,9 +78,6 @@ func subscribe( * "Supporting Subscriptions at Scale" information in the GraphQL specification. */ func createSourceEventStream( - queryStrategy: QueryFieldExecutionStrategy, - mutationStrategy: MutationFieldExecutionStrategy, - subscriptionStrategy: SubscriptionFieldExecutionStrategy, schema: GraphQLSchema, documentAST: Document, rootValue: any Sendable, @@ -100,9 +88,6 @@ func createSourceEventStream( // 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, schema: schema, documentAST: documentAST, rootValue: rootValue, diff --git a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift b/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift deleted file mode 100644 index 3caee2e0..00000000 --- a/Tests/GraphQLTests/FieldExecutionStrategyTests/FieldExecutionStrategyTests.swift +++ /dev/null @@ -1,297 +0,0 @@ -import Dispatch -@testable import GraphQL -import XCTest - -let queue = DispatchQueue(label: "testQueue") - -class FieldExecutionStrategyTests: XCTestCase { - enum StrategyError: Error { - case exampleError(msg: String) - } - - let schema = try! GraphQLSchema( - query: GraphQLObjectType( - name: "RootQueryType", - fields: [ - "sleep": GraphQLField( - type: GraphQLString, - resolve: { _, _, _, _ in - Thread.sleep(forTimeInterval: 0.1) - return "z" - } - ), - "bang": GraphQLField( - type: GraphQLString, - resolve: { (_, _, _, info: GraphQLResolveInfo) in - let group = DispatchGroup() - group.enter() - - queue.asyncAfter(wallDeadline: .now() + 0.1) { - group.leave() - } - - group.wait() - - throw StrategyError.exampleError( - msg: "\(info.fieldName): \(info.path.elements.last!)" - ) - } - ), - "futureBang": GraphQLField( - type: GraphQLString, - resolve: { (_, _, _, info: GraphQLResolveInfo) in - let g = DispatchGroup() - g.enter() - - queue.asyncAfter(wallDeadline: .now() + 0.1) { - g.leave() - } - - g.wait() - - throw 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 () async throws -> T) async throws - -> (value: T, seconds: Double) { - let start = DispatchTime.now() - let value = try await block() - let nanoseconds = DispatchTime.now().uptimeNanoseconds - start.uptimeNanoseconds - let seconds = Double(nanoseconds) / 1_000_000_000 - return ( - value: value, - seconds: seconds - ) - } - - func testSerialFieldExecutionStrategyWithSingleField() async throws { - let result = try await timing(await graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: singleQuery - )) - XCTAssertEqual(result.value, singleExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testSerialFieldExecutionStrategyWithSingleFieldError() async throws { - let result = try await timing(await graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: singleThrowsQuery - )) - XCTAssertEqual(result.value, singleThrowsExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testSerialFieldExecutionStrategyWithSingleFieldFailedFuture() async throws { - let result = try await timing(await graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: singleFailedFutureQuery - )) - XCTAssertEqual(result.value, singleFailedFutureExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testSerialFieldExecutionStrategyWithMultipleFields() async throws { - let result = try await timing(await graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: multiQuery - )) - XCTAssertEqual(result.value, multiExpected) -// XCTAssertEqualWithAccuracy(1.0, result.seconds, accuracy: 0.5) - } - - func testSerialFieldExecutionStrategyWithMultipleFieldErrors() async throws { - let result = try await timing(await graphql( - queryStrategy: SerialFieldExecutionStrategy(), - schema: schema, - request: multiThrowsQuery - )) - 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 testConcurrentFieldExecutionStrategyWithSingleField() async throws { - let result = try await timing(await graphql( - queryStrategy: ConcurrentFieldExecutionStrategy(), - schema: schema, - request: singleQuery - )) - XCTAssertEqual(result.value, singleExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testConcurrentFieldExecutionStrategyWithSingleFieldError() async throws { - let result = try await timing(await graphql( - queryStrategy: ConcurrentFieldExecutionStrategy(), - schema: schema, - request: singleThrowsQuery - )) - XCTAssertEqual(result.value, singleThrowsExpected) - // XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testConcurrentFieldExecutionStrategyWithMultipleFields() async throws { - let result = try await timing(await graphql( - queryStrategy: ConcurrentFieldExecutionStrategy(), - schema: schema, - request: multiQuery - )) - XCTAssertEqual(result.value, multiExpected) -// XCTAssertEqualWithAccuracy(0.1, result.seconds, accuracy: 0.25) - } - - func testConcurrentFieldExecutionStrategyWithMultipleFieldErrors() async throws { - let result = try await timing(await graphql( - queryStrategy: ConcurrentFieldExecutionStrategy(), - schema: schema, - request: multiThrowsQuery - )) - 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/SubscriptionTests/SubscriptionSchema.swift b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift index 0b65573c..beaf8600 100644 --- a/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift +++ b/Tests/GraphQLTests/SubscriptionTests/SubscriptionSchema.swift @@ -183,9 +183,6 @@ func createSubscription( variableValues: [String: Map] = [:] ) async throws -> AsyncThrowingStream { let result = try await graphqlSubscribe( - queryStrategy: SerialFieldExecutionStrategy(), - mutationStrategy: SerialFieldExecutionStrategy(), - subscriptionStrategy: SerialFieldExecutionStrategy(), schema: schema, request: query, rootValue: (), From 4f01de22fbb0aae988d4c2a2e3fa10c41721aeab Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 17:50:59 -0600 Subject: [PATCH 16/20] feat!: Uses specified rules by default Adjusts argument order --- MIGRATION.md | 19 +++++++++++++++++++ Sources/GraphQL/GraphQL.swift | 8 ++++---- Sources/GraphQL/Validation/Validate.swift | 19 +------------------ 3 files changed, 24 insertions(+), 22 deletions(-) diff --git a/MIGRATION.md b/MIGRATION.md index f29cd72a..3eda163e 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -20,6 +20,25 @@ With the conversion from NIO to Swift Concurrency, types used across async bound 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 diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index 10041e57..b3873570 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -80,13 +80,13 @@ public struct GraphQLErrors: Error, Sendable { /// and there will be an error inside `errors` specifying the reason for the failure and the path of /// the failed field. public func graphql( - validationRules: [@Sendable (ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, rootValue: (any Sendable) = (), context: (any Sendable) = (), variableValues: [String: Map] = [:], - operationName: String? = nil + operationName: String? = nil, + validationRules: [@Sendable (ValidationContext) -> Visitor] = specifiedRules ) async throws -> GraphQLResult { let source = Source(body: request, name: "GraphQL request") let documentAST = try parse(source: source) @@ -192,13 +192,13 @@ 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( - validationRules: [@Sendable (ValidationContext) -> Visitor] = [], schema: GraphQLSchema, request: String, rootValue: (any Sendable) = (), context: (any Sendable) = (), variableValues: [String: Map] = [:], - operationName: String? = nil + 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(source: source) diff --git a/Sources/GraphQL/Validation/Validate.swift b/Sources/GraphQL/Validation/Validate.swift index 6144e290..27e72602 100644 --- a/Sources/GraphQL/Validation/Validate.swift +++ b/Sources/GraphQL/Validation/Validate.swift @@ -1,20 +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: -/// 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( - schema: GraphQLSchema, - ast: Document -) -> [GraphQLError] { - return validate(schema: schema, ast: ast, rules: []) -} - /** * Implements the "Validation" section of the spec. * @@ -31,7 +14,7 @@ public func validate( public func validate( schema: GraphQLSchema, ast: Document, - rules: [@Sendable (ValidationContext) -> Visitor] + rules: [@Sendable (ValidationContext) -> Visitor] = specifiedRules ) -> [GraphQLError] { let typeInfo = TypeInfo(schema: schema) let rules = rules.isEmpty ? specifiedRules : rules From dfa0a607ce30a308e2111c6426f88cc9543f288b Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 18:38:05 -0600 Subject: [PATCH 17/20] feat!: Validates schema on execute --- Sources/GraphQL/GraphQL.swift | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Sources/GraphQL/GraphQL.swift b/Sources/GraphQL/GraphQL.swift index b3873570..cd66ba88 100644 --- a/Sources/GraphQL/GraphQL.swift +++ b/Sources/GraphQL/GraphQL.swift @@ -88,18 +88,27 @@ public func graphql( 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(source: source) + + // Validate let validationErrors = validate( schema: schema, ast: documentAST, rules: validationRules ) - guard validationErrors.isEmpty else { return GraphQLResult(errors: validationErrors) } + // Execute return try await execute( schema: schema, documentAST: documentAST, From 0522fb67807725984ecfe0218ed7b31979c433d2 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 22:06:11 -0600 Subject: [PATCH 18/20] feat: Exposes validateSchema --- Sources/GraphQL/Type/Validation.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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. From 4424f8e6825ee7b94c7ccb0feb706381b0c375b6 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 22:53:30 -0600 Subject: [PATCH 19/20] test: Resolve test warnings --- .../GraphQLTests/ValidationTests/ValidationTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift index 15d4e5ef..4e600898 100644 --- a/Tests/GraphQLTests/ValidationTests/ValidationTests.swift +++ b/Tests/GraphQLTests/ValidationTests/ValidationTests.swift @@ -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 { @@ -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) From 210fb722c6e9117bd87f87a882b1329a29f547b6 Mon Sep 17 00:00:00 2001 From: Jay Herron Date: Sun, 10 Aug 2025 23:29:18 -0600 Subject: [PATCH 20/20] chore: Fixes deprecation warnings --- Sources/GraphQL/Subscription/Subscribe.swift | 3 ++- Tests/GraphQLTests/LanguageTests/ParserTests.swift | 2 +- Tests/GraphQLTests/LanguageTests/PrinterTests.swift | 2 +- Tests/GraphQLTests/LanguageTests/SchemaParserTests.swift | 2 +- Tests/GraphQLTests/LanguageTests/SchemaPrinterTests.swift | 2 +- Tests/GraphQLTests/LanguageTests/VisitorTests.swift | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/Sources/GraphQL/Subscription/Subscribe.swift b/Sources/GraphQL/Subscription/Subscribe.swift index 5a82a1cc..56fed7da 100644 --- a/Sources/GraphQL/Subscription/Subscribe.swift +++ b/Sources/GraphQL/Subscription/Subscribe.swift @@ -39,7 +39,8 @@ func subscribe( guard let eventPayload = try await iterator.next() else { return nil } - // Despite the warning, we must force unwrap because on optional unwrap, compiler throws: + // 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( 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 edc28c7e..9ca4b045 100644 --- a/Tests/GraphQLTests/LanguageTests/VisitorTests.swift +++ b/Tests/GraphQLTests/LanguageTests/VisitorTests.swift @@ -504,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