Skip to content

Commit bbe0d98

Browse files
Merge branch 'master' into custom-validation-rules
2 parents cc0098b + 3071750 commit bbe0d98

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