Skip to content

Commit 3071750

Browse files
Merge pull request #100 from NeedleInAJayStack/feature/SwiftConcurrency
Swift Concurrency Support
2 parents 283cc4d + ea62340 commit 3071750

File tree

6 files changed

+1409
-0
lines changed

6 files changed

+1409
-0
lines changed

Sources/GraphQL/GraphQL.swift

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,3 +226,115 @@ public func graphqlSubscribe(
226226
operationName: operationName
227227
)
228228
}
229+
230+
// MARK: Async/Await
231+
232+
#if compiler(>=5.5) && canImport(_Concurrency)
233+
234+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
235+
/// This is the primary entry point function for fulfilling GraphQL operations
236+
/// by parsing, validating, and executing a GraphQL document along side a
237+
/// GraphQL schema.
238+
///
239+
/// More sophisticated GraphQL servers, such as those which persist queries,
240+
/// may wish to separate the validation and execution phases to a static time
241+
/// tooling step, and a server runtime step.
242+
///
243+
/// - parameter queryStrategy: The field execution strategy to use for query requests
244+
/// - parameter mutationStrategy: The field execution strategy to use for mutation requests
245+
/// - parameter subscriptionStrategy: The field execution strategy to use for subscription requests
246+
/// - parameter instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages.
247+
/// - parameter schema: The GraphQL type system to use when validating and executing a query.
248+
/// - parameter request: A GraphQL language formatted string representing the requested operation.
249+
/// - parameter rootValue: The value provided as the first argument to resolver functions on the top level type (e.g. the query object type).
250+
/// - parameter contextValue: A context value provided to all resolver functions functions
251+
/// - parameter variableValues: A mapping of variable name to runtime value to use for all variables defined in the `request`.
252+
/// - parameter operationName: The name of the operation to use if `request` contains multiple possible operations. Can be omitted if `request` contains only one operation.
253+
///
254+
/// - throws: throws GraphQLError if an error occurs while parsing the `request`.
255+
///
256+
/// - 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.
257+
public func graphql(
258+
queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(),
259+
mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(),
260+
subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(),
261+
instrumentation: Instrumentation = NoOpInstrumentation,
262+
schema: GraphQLSchema,
263+
request: String,
264+
rootValue: Any = (),
265+
context: Any = (),
266+
eventLoopGroup: EventLoopGroup,
267+
variableValues: [String: Map] = [:],
268+
operationName: String? = nil
269+
) async throws -> GraphQLResult {
270+
return try await graphql(
271+
queryStrategy: queryStrategy,
272+
mutationStrategy: mutationStrategy,
273+
subscriptionStrategy: subscriptionStrategy,
274+
instrumentation: instrumentation,
275+
schema: schema,
276+
request: request,
277+
rootValue: rootValue,
278+
context: context,
279+
eventLoopGroup: eventLoopGroup,
280+
variableValues: variableValues,
281+
operationName: operationName
282+
).get()
283+
}
284+
285+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
286+
/// This is the primary entry point function for fulfilling GraphQL subscription
287+
/// operations by parsing, validating, and executing a GraphQL subscription
288+
/// document along side a GraphQL schema.
289+
///
290+
/// More sophisticated GraphQL servers, such as those which persist queries,
291+
/// may wish to separate the validation and execution phases to a static time
292+
/// tooling step, and a server runtime step.
293+
///
294+
/// - parameter queryStrategy: The field execution strategy to use for query requests
295+
/// - parameter mutationStrategy: The field execution strategy to use for mutation requests
296+
/// - parameter subscriptionStrategy: The field execution strategy to use for subscription requests
297+
/// - parameter instrumentation: The instrumentation implementation to call during the parsing, validating, execution, and field resolution stages.
298+
/// - parameter schema: The GraphQL type system to use when validating and executing a query.
299+
/// - parameter request: A GraphQL language formatted string representing the requested operation.
300+
/// - parameter rootValue: The value provided as the first argument to resolver functions on the top level type (e.g. the query object type).
301+
/// - parameter contextValue: A context value provided to all resolver functions
302+
/// - parameter variableValues: A mapping of variable name to runtime value to use for all variables defined in the `request`.
303+
/// - parameter operationName: The name of the operation to use if `request` contains multiple possible operations. Can be omitted if `request` contains only one operation.
304+
///
305+
/// - throws: throws GraphQLError if an error occurs while parsing the `request`.
306+
///
307+
/// - returns: returns a SubscriptionResult containing the subscription observable inside the key `observable` and any validation or execution errors inside the key `errors`. The
308+
/// 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
309+
/// 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`.
310+
/// 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
311+
/// will be an error inside `errors` specifying the reason for the failure and the path of the failed field.
312+
public func graphqlSubscribe(
313+
queryStrategy: QueryFieldExecutionStrategy = SerialFieldExecutionStrategy(),
314+
mutationStrategy: MutationFieldExecutionStrategy = SerialFieldExecutionStrategy(),
315+
subscriptionStrategy: SubscriptionFieldExecutionStrategy = SerialFieldExecutionStrategy(),
316+
instrumentation: Instrumentation = NoOpInstrumentation,
317+
schema: GraphQLSchema,
318+
request: String,
319+
rootValue: Any = (),
320+
context: Any = (),
321+
eventLoopGroup: EventLoopGroup,
322+
variableValues: [String: Map] = [:],
323+
operationName: String? = nil
324+
) async throws -> SubscriptionResult {
325+
return try await graphqlSubscribe(
326+
queryStrategy: queryStrategy,
327+
mutationStrategy: mutationStrategy,
328+
subscriptionStrategy: subscriptionStrategy,
329+
instrumentation: instrumentation,
330+
schema: schema,
331+
request: request,
332+
rootValue: rootValue,
333+
context: context,
334+
eventLoopGroup: eventLoopGroup,
335+
variableValues: variableValues,
336+
operationName: operationName
337+
).get()
338+
}
339+
340+
#endif

Sources/GraphQL/Subscription/EventStream.swift

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,51 @@ open class EventStream<Element> {
66
fatalError("This function should be overridden by implementing classes")
77
}
88
}
9+
10+
#if compiler(>=5.5) && canImport(_Concurrency)
11+
12+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
13+
/// Event stream that wraps an `AsyncThrowingStream` from Swift's standard concurrency system.
14+
public class ConcurrentEventStream<Element>: EventStream<Element> {
15+
public let stream: AsyncThrowingStream<Element, Error>
16+
17+
public init(_ stream: AsyncThrowingStream<Element, Error>) {
18+
self.stream = stream
19+
}
20+
21+
/// Performs the closure on each event in the current stream and returns a stream of the results.
22+
/// - Parameter closure: The closure to apply to each event in the stream
23+
/// - Returns: A stream of the results
24+
override open func map<To>(_ closure: @escaping (Element) throws -> To) -> ConcurrentEventStream<To> {
25+
let newStream = self.stream.mapStream(closure)
26+
return ConcurrentEventStream<To>.init(newStream)
27+
}
28+
}
29+
30+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
31+
extension AsyncThrowingStream {
32+
func mapStream<To>(_ closure: @escaping (Element) throws -> To) -> AsyncThrowingStream<To, Error> {
33+
return AsyncThrowingStream<To, Error> { continuation in
34+
Task {
35+
for try await event in self {
36+
let newEvent = try closure(event)
37+
continuation.yield(newEvent)
38+
}
39+
}
40+
}
41+
}
42+
43+
func filterStream(_ isIncluded: @escaping (Element) throws -> Bool) -> AsyncThrowingStream<Element, Error> {
44+
return AsyncThrowingStream<Element, Error> { continuation in
45+
Task {
46+
for try await event in self {
47+
if try isIncluded(event) {
48+
continuation.yield(event)
49+
}
50+
}
51+
}
52+
}
53+
}
54+
}
55+
56+
#endif

Tests/GraphQLTests/HelloWorldTests/HelloWorldTests.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,28 @@ class HelloWorldTests : XCTestCase {
6262

6363
XCTAssertEqual(result, expected)
6464
}
65+
66+
#if compiler(>=5.5) && canImport(_Concurrency)
67+
68+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
69+
func testHelloAsync() async throws {
70+
let group = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)
71+
72+
defer {
73+
XCTAssertNoThrow(try group.syncShutdownGracefully())
74+
}
75+
76+
let query = "{ hello }"
77+
let expected = GraphQLResult(data: ["hello": "world"])
78+
79+
let result = try await graphql(
80+
schema: schema,
81+
request: query,
82+
eventLoopGroup: group
83+
)
84+
85+
XCTAssertEqual(result, expected)
86+
}
87+
88+
#endif
6589
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import GraphQL
2+
3+
4+
#if compiler(>=5.5) && canImport(_Concurrency)
5+
6+
@available(macOS 12, iOS 15, watchOS 8, tvOS 15, *)
7+
/// A very simple publish/subscriber used for testing
8+
class SimplePubSub<T> {
9+
private var subscribers: [Subscriber<T>]
10+
11+
init() {
12+
subscribers = []
13+
}
14+
15+
func emit(event: T) {
16+
for subscriber in subscribers {
17+
subscriber.callback(event)
18+
}
19+
}
20+
21+
func cancel() {
22+
for subscriber in subscribers {
23+
subscriber.cancel()
24+
}
25+
}
26+
27+
func subscribe() -> ConcurrentEventStream<T> {
28+
let asyncStream = AsyncThrowingStream<T, Error> { continuation in
29+
let subscriber = Subscriber<T>(
30+
callback: { newValue in
31+
continuation.yield(newValue)
32+
},
33+
cancel: {
34+
continuation.finish()
35+
}
36+
)
37+
subscribers.append(subscriber)
38+
return
39+
}
40+
return ConcurrentEventStream<T>.init(asyncStream)
41+
}
42+
}
43+
44+
struct Subscriber<T> {
45+
let callback: (T) -> Void
46+
let cancel: () -> Void
47+
}
48+
49+
#endif

0 commit comments

Comments
 (0)