Skip to content

Commit 8e8eb89

Browse files
authored
Concurrency Stage 1: Introduces initial wrapper-style async/await support (#184)
* Introduces a wrapper-style async/await approach originally implemented by @elfenlaid * Adds the original implementation + improved header docs with usage caveats * Updates the API.swift based on recent changes that resolve Swift 6 errors (in future) * Update Sources/SwiftGraphQLClient/Client/Core.swift
1 parent 5255993 commit 8e8eb89

File tree

7 files changed

+272
-18
lines changed

7 files changed

+272
-18
lines changed

Sources/SwiftGraphQLClient/Client/Core.swift

+36-2
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,27 @@ public class Client: GraphQLClient, ObservableObject {
255255

256256
return self.execute(operation: operation)
257257
}
258-
258+
259+
/// Executes a query request with given execution parameters.
260+
///
261+
/// Note: While this behaves much the same as the published-based
262+
/// APIs, async/await inherently does __not__ support multiple
263+
/// return values. If you expect multiple values from an async/await
264+
/// API, please use the corresponding publisher API instead.
265+
///
266+
/// Additionally, due to the differences between async/await and
267+
/// Combine publishers, the async APIs will only return a single value,
268+
/// even if the query is invalidated. Therefore if you currently
269+
/// rely on invalidation behaviour provided by publishers we suggest
270+
/// you continue to use the Combine APIs.
271+
public func query(
272+
_ args: ExecutionArgs,
273+
request: URLRequest? = nil,
274+
policy: Operation.Policy = .cacheFirst
275+
) async -> OperationResult {
276+
await self.query(args, request: request, policy: policy).first()
277+
}
278+
259279
/// Executes a mutation request with given execution parameters.
260280
public func mutate(
261281
_ args: ExecutionArgs,
@@ -273,7 +293,21 @@ public class Client: GraphQLClient, ObservableObject {
273293

274294
return self.execute(operation: operation)
275295
}
276-
296+
297+
/// Executes a mutation request with given execution parameters.
298+
///
299+
/// Note: While this behaves much the same as the published-based
300+
/// APIs, async/await inherently does __not__ support multiple
301+
/// return values. If you expect multiple values from an async/await
302+
/// API, please use the corresponding publisher API instead.
303+
public func mutate(
304+
_ args: ExecutionArgs,
305+
request: URLRequest? = nil,
306+
policy: Operation.Policy = .cacheFirst
307+
) async -> OperationResult {
308+
await self.mutate(args, request: request, policy: policy).first()
309+
}
310+
277311
/// Executes a subscription request with given execution parameters.
278312
public func subscribe(
279313
_ args: ExecutionArgs,

Sources/SwiftGraphQLClient/Client/Selection.swift

+22-4
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,7 @@ extension GraphQLClient {
7777
}
7878

7979
// MARK: - Decoders
80-
81-
80+
8281
/// Executes a query and returns a stream of decoded values.
8382
public func query<T, TypeLock>(
8483
_ selection: Selection<T, TypeLock>,
@@ -97,7 +96,17 @@ extension GraphQLClient {
9796
}
9897
.eraseToAnyPublisher()
9998
}
100-
99+
100+
/// Executes a query request with given execution parameters.
101+
public func query<T, TypeLock>(
102+
_ selection: Selection<T, TypeLock>,
103+
as operationName: String? = nil,
104+
request: URLRequest? = nil,
105+
policy: Operation.Policy = .cacheFirst
106+
) async throws -> DecodedOperationResult<T> where TypeLock: GraphQLHttpOperation {
107+
try await self.query(selection, as: operationName, request: request, policy: policy).first()
108+
}
109+
101110
/// Executes a mutation and returns a stream of decoded values.
102111
public func mutate<T, TypeLock>(
103112
_ selection: Selection<T, TypeLock>,
@@ -116,7 +125,16 @@ extension GraphQLClient {
116125
}
117126
.eraseToAnyPublisher()
118127
}
119-
128+
129+
public func mutate<T, TypeLock>(
130+
_ selection: Selection<T, TypeLock>,
131+
as operationName: String? = nil,
132+
request: URLRequest? = nil,
133+
policy: Operation.Policy = .cacheFirst
134+
) async throws -> DecodedOperationResult<T> where TypeLock: GraphQLHttpOperation {
135+
try await self.mutate(selection, as: operationName, request: request, policy: policy).first()
136+
}
137+
120138
/// Creates a subscription stream of decoded values from the given query.
121139
public func subscribe<T, TypeLock>(
122140
to selection: Selection<T, TypeLock>,

Sources/SwiftGraphQLClient/Extensions/Publishers+Extensions.swift

+41
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,47 @@ extension Publisher {
88
public func takeUntil<Terminator: Publisher>(_ terminator: Terminator) -> Publishers.TakenUntilPublisher<Self, Terminator> {
99
Publishers.TakenUntilPublisher<Self, Terminator>(upstream: self, terminator: terminator)
1010
}
11+
12+
/// Takes the first emitted value and completes or throws an error
13+
func first() async throws -> Output {
14+
try await withCheckedThrowingContinuation { continuation in
15+
var cancellable: AnyCancellable?
16+
17+
cancellable = first()
18+
.sink { result in
19+
switch result {
20+
case .finished:
21+
break
22+
case let .failure(error):
23+
continuation.resume(throwing: error)
24+
}
25+
cancellable?.cancel()
26+
} receiveValue: { value in
27+
continuation.resume(with: .success(value))
28+
}
29+
}
30+
}
31+
}
32+
33+
extension Publisher where Failure == Never {
34+
/// Takes the first emitted value and completes or throws an error
35+
///
36+
/// Note: While this behaves much the same as the published-based
37+
/// APIs, async/await inherently does __not__ support multiple
38+
/// return values. If you expect multiple values from an async/await
39+
/// API, please use the corresponding publisher API instead.
40+
func first() async -> Output {
41+
await withCheckedContinuation { continuation in
42+
var cancellable: AnyCancellable?
43+
44+
cancellable = first()
45+
.sink { _ in
46+
cancellable?.cancel()
47+
} receiveValue: { value in
48+
continuation.resume(with: .success(value))
49+
}
50+
}
51+
}
1152
}
1253

1354
extension Publishers {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import GraphQL
2+
import SwiftGraphQLClient
3+
import XCTest
4+
import Combine
5+
import SwiftGraphQL
6+
7+
final class AsyncInterfaceTests: XCTestCase {
8+
func testAsyncSelectionQueryReturnsValue() async throws {
9+
let selection = Selection<String, Objects.User> {
10+
try $0.id()
11+
}
12+
13+
let client = MockClient(customExecute: { operation in
14+
let id = GraphQLField.leaf(field: "id", parent: "User", arguments: [])
15+
16+
let user = GraphQLField.composite(
17+
field: "user",
18+
parent: "Query",
19+
type: "User",
20+
arguments: [],
21+
selection: selection.__selection()
22+
)
23+
24+
let result = OperationResult(
25+
operation: operation,
26+
data: [
27+
user.alias!: [
28+
id.alias!: "123"
29+
]
30+
],
31+
error: nil
32+
)
33+
return Just(result).eraseToAnyPublisher()
34+
})
35+
36+
37+
let result = try await client.query(Objects.Query.user(selection: selection))
38+
XCTAssertEqual(result.data, "123")
39+
}
40+
41+
func testAsyncSelectionQueryThrowsError() async throws {
42+
let selection = Selection<String, Objects.User> {
43+
try $0.id()
44+
}
45+
46+
let client = MockClient(customExecute: { operation in
47+
let result = OperationResult(
48+
operation: operation,
49+
data: ["unknown_field": "123"],
50+
error: nil
51+
)
52+
return Just(result).eraseToAnyPublisher()
53+
})
54+
55+
await XCTAssertThrowsError(of: ObjectDecodingError.self) {
56+
try await client.query(Objects.Query.user(selection: selection))
57+
}
58+
}
59+
60+
func testAsyncSelectionMutationReturnsValue() async throws {
61+
let selection = Selection.AuthPayload<String?> {
62+
try $0.on(
63+
authPayloadSuccess: Selection.AuthPayloadSuccess<String?> {
64+
try $0.token()
65+
},
66+
authPayloadFailure: Selection.AuthPayloadFailure<String?> { _ in
67+
nil
68+
}
69+
)
70+
}
71+
72+
let client = MockClient(customExecute: { operation in
73+
let token = GraphQLField.leaf(field: "token", parent: "AuthPayloadSuccess", arguments: [])
74+
75+
let auth = GraphQLField.composite(
76+
field: "auth",
77+
parent: "Mutation",
78+
type: "AuthPayload",
79+
arguments: [],
80+
selection: selection.__selection()
81+
)
82+
83+
let result = OperationResult(
84+
operation: operation,
85+
data: [
86+
auth.alias!: [
87+
"__typename": "AuthPayloadSuccess",
88+
token.alias!: "123"
89+
]
90+
],
91+
error: nil
92+
)
93+
return Just(result).eraseToAnyPublisher()
94+
})
95+
96+
let result = try await client.mutate(Objects.Mutation.auth(selection: selection))
97+
XCTAssertEqual(result.data, "123")
98+
}
99+
100+
func testAsyncSelectionMutationThrowsError() async throws {
101+
let selection = Selection.AuthPayload<String?> {
102+
try $0.on(
103+
authPayloadSuccess: Selection.AuthPayloadSuccess<String?> {
104+
try $0.token()
105+
},
106+
authPayloadFailure: Selection.AuthPayloadFailure<String?> { _ in
107+
nil
108+
}
109+
)
110+
}
111+
112+
let client = MockClient(customExecute: { operation in
113+
let result = OperationResult(
114+
operation: operation,
115+
data: ["unknown_field": "123"],
116+
error: nil
117+
)
118+
return Just(result).eraseToAnyPublisher()
119+
})
120+
121+
await XCTAssertThrowsError(of: ObjectDecodingError.self) {
122+
try await client.mutate(Objects.Mutation.auth(selection: selection))
123+
}
124+
}
125+
}

Tests/SwiftGraphQLClientTests/Extensions/Publishers+ExtensionsTests.swift

+20
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,24 @@ final class PublishersExtensionsTests: XCTestCase {
116116

117117
XCTAssertEqual(received, [1])
118118
}
119+
120+
func testTakeTheFirstEmittedValueAsynchronously() async throws {
121+
let value = await Just(1).first()
122+
XCTAssertEqual(value, 1)
123+
}
124+
125+
func testTakeTheFirstEmittedValueAsynchronouslyFromThrowingPublisher() async throws {
126+
struct TestError: Error {}
127+
128+
let value = try await Just(1).setFailureType(to: TestError.self).first()
129+
XCTAssertEqual(value, 1)
130+
}
131+
132+
func testThrowEmittedErrorAsynchronously() async throws {
133+
struct TestError: Error {}
134+
135+
await XCTAssertThrowsError(of: TestError.self) {
136+
try await Fail<Int, TestError>(error: TestError()).first()
137+
}
138+
}
119139
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import XCTest
2+
3+
/// Checks whether the provided `body` throws an `error` of the given `error`'s type
4+
func XCTAssertThrowsError<T: Swift.Error, Output>(
5+
of: T.Type,
6+
file: StaticString = #file,
7+
line: UInt = #line,
8+
_ body: () async throws -> Output
9+
) async {
10+
do {
11+
_ = try await body()
12+
XCTFail("body completed successfully", file: file, line: line)
13+
} catch let error {
14+
XCTAssertNotNil(error as? T, "Expected error of \(T.self), got \(type(of: error))")
15+
}
16+
}

0 commit comments

Comments
 (0)