Skip to content

Commit 8eef5aa

Browse files
authored
feat: Add Combine support (#667)
- feat: Amplify Category APIs that accept optional listeners now default them to `nil`, simplifying the call site - feat: HubChannel is now Hashable - feat: Add 'underlying error' constructor to AmplifyError - chore: Fix OperationTestBase and reenable previously disabled tests - chore: Change GraphQLOperation and GraphQLSubscriptionOperation to subclasses rather than typealiases
1 parent 0a357a7 commit 8eef5aa

File tree

60 files changed

+4331
-216
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

60 files changed

+4331
-216
lines changed

Amplify.xcodeproj/project.pbxproj

Lines changed: 79 additions & 3 deletions
Large diffs are not rendered by default.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
// No-listener versions of the public APIs, to clean call sites that use Combine
9+
// publishers to get results
10+
11+
extension APICategoryGraphQLBehavior {
12+
public func query<R: Decodable>(request: GraphQLRequest<R>) -> GraphQLOperation<R> {
13+
query(request: request, listener: nil)
14+
}
15+
16+
public func mutate<R: Decodable>(request: GraphQLRequest<R>) -> GraphQLOperation<R> {
17+
mutate(request: request, listener: nil)
18+
}
19+
20+
public func subscribe<R>(request: GraphQLRequest<R>) -> GraphQLSubscriptionOperation<R> {
21+
subscribe(request: request, valueListener: nil, completionListener: nil)
22+
}
23+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Foundation
9+
10+
// No-listener versions of the public APIs, to clean call sites that use Combine
11+
// publishers to get results
12+
13+
extension APICategoryRESTBehavior {
14+
/// Perform an HTTP GET operation
15+
///
16+
/// - Parameter request: Contains information such as path, query parameters, body.
17+
/// - Returns: An operation that can be observed for its value
18+
public func get(request: RESTRequest) -> RESTOperation {
19+
get(request: request, listener: nil)
20+
}
21+
22+
/// Perform an HTTP PUT operation
23+
///
24+
/// - Parameter request: Contains information such as path, query parameters, body.
25+
/// - Returns: An operation that can be observed for its value
26+
public func put(request: RESTRequest) -> RESTOperation {
27+
put(request: request, listener: nil)
28+
}
29+
30+
/// Perform an HTTP POST operation
31+
///
32+
/// - Parameter request: Contains information such as path, query parameters, body.
33+
/// - Returns: An operation that can be observed for its value
34+
public func post(request: RESTRequest) -> RESTOperation {
35+
post(request: request, listener: nil)
36+
}
37+
38+
/// Perform an HTTP DELETE operation
39+
///
40+
/// - Parameter request: Contains information such as path, query parameters, body.
41+
/// - Returns: An operation that can be observed for its value
42+
public func delete(request: RESTRequest) -> RESTOperation {
43+
delete(request: request, listener: nil)
44+
}
45+
46+
/// Perform an HTTP HEAD operation
47+
///
48+
/// - Parameter request: Contains information such as path, query parameters, body.
49+
/// - Returns: An operation that can be observed for its value
50+
public func head(request: RESTRequest) -> RESTOperation {
51+
head(request: request, listener: nil)
52+
}
53+
54+
/// Perform an HTTP PATCH operation
55+
///
56+
/// - Parameter request: Contains information such as path, query parameters, body.
57+
/// - Returns: An operation that can be observed for its value
58+
public func patch(request: RESTRequest) -> RESTOperation {
59+
patch(request: request, listener: nil)
60+
}
61+
}

Amplify/Categories/API/Error/APIError.swift

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@ import Foundation
99

1010
/// Errors specific to the API Category
1111
public enum APIError {
12-
1312
public typealias UserInfo = [String: Any]
1413
public typealias StatusCode = Int
1514

@@ -37,6 +36,18 @@ public enum APIError {
3736
}
3837

3938
extension APIError: AmplifyError {
39+
public init(
40+
errorDescription: ErrorDescription = "An unknown error occurred",
41+
recoverySuggestion: RecoverySuggestion = "See `underlyingError` for more details",
42+
error: Error
43+
) {
44+
if let error = error as? Self {
45+
self = error
46+
} else {
47+
self = .unknown(errorDescription, recoverySuggestion, error)
48+
}
49+
}
50+
4051
public var errorDescription: ErrorDescription {
4152
switch self {
4253
case .unknown(let errorDescription, _, _):
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
//
2+
// Copyright 2018-2020 Amazon.com,
3+
// Inc. or its affiliates. All Rights Reserved.
4+
//
5+
// SPDX-License-Identifier: Apache-2.0
6+
//
7+
8+
import Combine
9+
import Foundation
10+
11+
// MARK: - GraphQLOperation
12+
13+
@available(iOS 13.0, *)
14+
public extension GraphQLOperation {
15+
/// Publishes the final result of the operation
16+
var resultPublisher: AnyPublisher<Success, Failure> {
17+
internalResultPublisher
18+
}
19+
}
20+
21+
// MARK: - GraphQLSubscriptionOperation
22+
23+
@available(iOS 13.0, *)
24+
public extension GraphQLSubscriptionOperation {
25+
26+
/// Publishes the state of the GraphQL subscription's underlying network connection.
27+
///
28+
/// Subscription termination will be reported as a `completion` on the
29+
/// `subscriptionDataPublisher` completion, so this is really only useful if you
30+
/// want to monitor the `.connected` state.
31+
var connectionStatePublisher: AnyPublisher<SubscriptionConnectionState, APIError> {
32+
// Suppress Void results from the result publisher, but continue to emit
33+
// completions
34+
let transformedResultPublisher = internalResultPublisher
35+
.flatMap { _ in Empty<SubscriptionConnectionState, Failure>(completeImmediately: true) }
36+
37+
// Transform the in-process publisher to one that only outputs connectionState events
38+
let transformedInProcessPublisher = internalInProcessPublisher
39+
.compactMap { event -> SubscriptionConnectionState? in
40+
switch event {
41+
case .connection(let state):
42+
return state
43+
default:
44+
return nil
45+
}
46+
}
47+
.setFailureType(to: Failure.self)
48+
49+
// Now that the publisher signatures match, we can merge them
50+
return transformedResultPublisher
51+
.merge(with: transformedInProcessPublisher)
52+
.eraseToAnyPublisher()
53+
}
54+
55+
/// Publishes the data received from a GraphQL subscription.
56+
///
57+
/// The publisher emits `GraphQLResponse` events, which are standard Swift `Result`
58+
/// values that contain either a successfully decoded response value, or a
59+
/// `GraphQLResponseError` describing the reason that a value could not be
60+
/// successfully decoded. Receiving a `.failure` response does not mean the
61+
/// subscription is terminated--the subscription may still receive values, and each
62+
/// value is independently evaluated. Thus, you may see a data stream containing a
63+
/// mix of successfully decoded responses, partially decoded responses, or decoding
64+
/// errors, none of which affect the state of the underlying subscription
65+
/// connection.
66+
///
67+
/// When the subscription terminates with a cancellation or disconnection, this
68+
/// publisher will receive a `completion`.
69+
var subscriptionDataPublisher: AnyPublisher<GraphQLResponse<R>, Failure> {
70+
// Suppress Void results from the result publisher, but continue to emit completions
71+
let transformedResultPublisher = internalResultPublisher
72+
.flatMap { _ in Empty<GraphQLResponse<R>, Failure>(completeImmediately: true) }
73+
74+
// Transform the in-process publisher to one that only outputs GraphQLResponse events
75+
let transformedInProcessPublisher = internalInProcessPublisher
76+
.compactMap { event -> GraphQLResponse<R>? in
77+
switch event {
78+
case .data(let result):
79+
return result
80+
default:
81+
return nil
82+
}
83+
}
84+
.setFailureType(to: Failure.self)
85+
86+
// Now that the publisher signatures match, we can merge them
87+
return transformedResultPublisher
88+
.merge(with: transformedInProcessPublisher)
89+
.eraseToAnyPublisher()
90+
}
91+
92+
}
93+
94+
// MARK: - RESTOperation
95+
96+
@available(iOS 13.0, *)
97+
public extension AmplifyOperation
98+
where
99+
Request == RESTOperation.Request,
100+
Success == RESTOperation.Success,
101+
Failure == RESTOperation.Failure {
102+
/// Publishes the final result of the operation
103+
var resultPublisher: AnyPublisher<Success, Failure> {
104+
internalResultPublisher
105+
}
106+
}

Amplify/Categories/API/Operation/GraphQLOperation.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,18 @@
55
// SPDX-License-Identifier: Apache-2.0
66
//
77

8-
public typealias GraphQLOperation<R: Decodable> = AmplifyOperation<
8+
open class GraphQLOperation<R: Decodable>: AmplifyOperation<
99
GraphQLOperationRequest<R>,
1010
GraphQLResponse<R>,
1111
APIError
12-
>
12+
> { }
1313

14-
public typealias GraphQLSubscriptionOperation<R: Decodable> = AmplifyInProcessReportingOperation<
14+
open class GraphQLSubscriptionOperation<R: Decodable>: AmplifyInProcessReportingOperation<
1515
GraphQLOperationRequest<R>,
1616
SubscriptionEvent<GraphQLResponse<R>>,
1717
Void,
1818
APIError
19-
>
19+
> { }
2020

2121
public extension HubPayload.EventName.API {
2222
/// eventName for HubPayloads emitted by this operation

Amplify/Categories/API/Response/GraphQLResponse.swift

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ public enum GraphQLResponseError<ResponseType: Decodable>: AmplifyError {
2828
/// response type. The RawGraphQLResponse contains the entire response from the service, including data and errors.
2929
case transformationError(RawGraphQLResponse, APIError)
3030

31+
/// An unknown error occurred
32+
case unknown(ErrorDescription, RecoverySuggestion, Error?)
33+
3134
public var errorDescription: ErrorDescription {
3235
switch self {
3336
case .error(let errors):
@@ -36,6 +39,8 @@ public enum GraphQLResponseError<ResponseType: Decodable>: AmplifyError {
3639
return "GraphQL service returned a partially-successful response containing errors: \(errors)"
3740
case .transformationError:
3841
return "Failed to decode GraphQL response to the `ResponseType` \(String(describing: ResponseType.self))"
42+
case .unknown(let errorDescription, _, _):
43+
return errorDescription
3944
}
4045
}
4146

@@ -50,6 +55,8 @@ public enum GraphQLResponseError<ResponseType: Decodable>: AmplifyError {
5055
Failed to transform to `ResponseType`.
5156
Take a look at the `RawGraphQLResponse` and underlying error to see where it failed to decode.
5257
"""
58+
case .unknown(_, let recoverySuggestion, _):
59+
return recoverySuggestion
5360
}
5461
}
5562

@@ -61,6 +68,21 @@ public enum GraphQLResponseError<ResponseType: Decodable>: AmplifyError {
6168
return nil
6269
case .transformationError(_, let error):
6370
return error
71+
case .unknown(_, _, let error):
72+
return error
73+
}
74+
}
75+
76+
public init(
77+
errorDescription: ErrorDescription = "An unknown error occurred",
78+
recoverySuggestion: RecoverySuggestion = "See `underlyingError` for more details",
79+
error: Error
80+
) {
81+
if let error = error as? Self {
82+
self = error
83+
} else {
84+
self = .unknown(errorDescription, recoverySuggestion, error)
6485
}
6586
}
87+
6688
}

Amplify/Categories/Analytics/Error/AnalyticsError.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,17 @@ extension AnalyticsError: AmplifyError {
3939
return error
4040
}
4141
}
42+
43+
public init(
44+
errorDescription: ErrorDescription = "An unknown error occurred",
45+
recoverySuggestion: RecoverySuggestion = "(Ignored)",
46+
error: Error
47+
) {
48+
if let error = error as? Self {
49+
self = error
50+
} else {
51+
self = .unknown(errorDescription, error)
52+
}
53+
}
54+
4255
}

0 commit comments

Comments
 (0)