Skip to content

Commit cdfe36c

Browse files
committed
Refactor OAuth2 functions: Migrate from callbacks to async/await (Swift concurrency)
- Rewrote OAuth2 functions to utilize async/await syntax for improved readability and maintainability. - Leveraged Swift concurrency to streamline asynchronous operations and reduce callback complexity.
1 parent cbbf5c1 commit cdfe36c

11 files changed

+311
-428
lines changed

Sources/Base/OAuth2Base.swift

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -132,14 +132,8 @@ open class OAuth2Base: OAuth2Securable {
132132
set { clientConfig.customUserAgent = newValue }
133133
}
134134

135-
136-
/// This closure is internally used with `authorize(params:callback:)` and only exposed for subclassing reason, do not mess with it!
137-
public final var didAuthorizeOrFail: ((_ parameters: OAuth2JSON?, _ error: OAuth2Error?) -> Void)?
138-
139135
/// Returns true if the receiver is currently authorizing.
140-
public final var isAuthorizing: Bool {
141-
return nil != didAuthorizeOrFail
142-
}
136+
public final var isAuthorizing: Bool = false
143137

144138
/// Returns true if the receiver is currently exchanging the refresh token.
145139
public final var isExchangingRefreshToken: Bool = false
@@ -277,8 +271,7 @@ open class OAuth2Base: OAuth2Securable {
277271
storeTokensToKeychain()
278272
}
279273
callOnMainThread() {
280-
self.didAuthorizeOrFail?(parameters, nil)
281-
self.didAuthorizeOrFail = nil
274+
self.isAuthorizing = false
282275
self.internalAfterAuthorizeOrFail?(false, nil)
283276
self.afterAuthorizeOrFail?(parameters, nil)
284277
}
@@ -301,8 +294,7 @@ open class OAuth2Base: OAuth2Securable {
301294
finalError = OAuth2Error.requestCancelled
302295
}
303296
callOnMainThread() {
304-
self.didAuthorizeOrFail?(nil, finalError)
305-
self.didAuthorizeOrFail = nil
297+
self.isAuthorizing = false
306298
self.internalAfterAuthorizeOrFail?(true, finalError)
307299
self.afterAuthorizeOrFail?(nil, finalError)
308300
}

Sources/Base/OAuth2RequestPerformer.swift

Lines changed: 7 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -17,14 +17,12 @@ The class `OAuth2DataTaskRequestPerformer` implements this protocol and is by de
1717
public protocol OAuth2RequestPerformer {
1818

1919
/**
20-
This method should start executing the given request, returning a URLSessionTask if it chooses to do so. **You do not neet to call
21-
`resume()` on this task**, it's supposed to already have started. It is being returned so you may be able to do additional stuff.
20+
This method should execute the given request asynchronously.
2221

2322
- parameter request: An URLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on.
24-
- parameter completionHandler: The completion handler to call when the load request is complete.
25-
- returns: An already running session task
23+
- returns: Data and response.
2624
*/
27-
func perform(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask?
25+
func perform(request: URLRequest) async throws -> (Data, URLResponse)
2826
}
2927

3028

@@ -36,7 +34,6 @@ open class OAuth2DataTaskRequestPerformer: OAuth2RequestPerformer {
3634
/// The URLSession that should be used.
3735
public var session: URLSession
3836

39-
4037
/**
4138
Designated initializer.
4239
*/
@@ -45,18 +42,13 @@ open class OAuth2DataTaskRequestPerformer: OAuth2RequestPerformer {
4542
}
4643

4744
/**
48-
This method should start executing the given request, returning a URLSessionTask if it chooses to do so. **You do not neet to call
49-
`resume()` on this task**, it's supposed to already have started. It is being returned so you may be able to do additional stuff.
45+
This method should execute the given request asynchronously.
5046

5147
- parameter request: An URLRequest object that provides the URL, cache policy, request type, body data or body stream, and so on.
52-
- parameter completionHandler: The completion handler to call when the load request is complete.
53-
- returns: An already running session data task
48+
- returns: Data and response.
5449
*/
55-
@discardableResult
56-
open func perform(request: URLRequest, completionHandler: @escaping (Data?, URLResponse?, Error?) -> Void) -> URLSessionTask? {
57-
let task = session.dataTask(with: request, completionHandler: completionHandler)
58-
task.resume()
59-
return task
50+
open func perform(request: URLRequest) async throws -> (Data, URLResponse) {
51+
try await session.data(for: request)
6052
}
6153
}
6254

Sources/Base/OAuth2Requestable.swift

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -105,41 +105,40 @@ open class OAuth2Requestable {
105105
open var requestPerformer: OAuth2RequestPerformer?
106106

107107
/**
108-
Perform the supplied request and call the callback with the response JSON dict or an error. This method is intended for authorization
108+
Perform the supplied request and return the response JSON dict or throw an error. This method is intended for authorization
109109
calls, not for data calls outside of the OAuth2 dance.
110110

111-
This implementation uses the shared `NSURLSession` and executes a data task. If the server responds with an error, this will be
112-
converted into an error according to information supplied in the response JSON (if availale).
113-
114-
The callback returns a response object that is easy to use, like so:
115-
116-
perform(request: req) { response in
117-
do {
118-
let data = try response.responseData()
119-
// do what you must with `data` as Data and `response.response` as HTTPURLResponse
120-
}
121-
catch let error {
122-
// the request failed because of `error`
123-
}
124-
}
125-
126-
Easy, right?
111+
This implementation uses the shared `NSURLSession`. If the server responds with an error, this will be
112+
converted into an error according to information supplied in the response JSON (if available).
127113

128114
- parameter request: The request to execute
129-
- parameter callback: The callback to call when the request completes/fails. Looks terrifying, see above on how to use it
115+
- returns : OAuth2 response
130116
*/
131-
open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) {
117+
open func perform(request: URLRequest) async -> OAuth2Response {
132118
self.logger?.trace("OAuth2", msg: "REQUEST\n\(request.debugDescription)\n---")
133119
let performer = requestPerformer ?? OAuth2DataTaskRequestPerformer(session: session)
134120
requestPerformer = performer
135-
let task = performer.perform(request: request) { sessData, sessResponse, error in
136-
self.abortableTask = nil
137-
self.logger?.trace("OAuth2", msg: "RESPONSE\n\(sessResponse?.debugDescription ?? "no response")\n\n\(String(data: sessData ?? Data(), encoding: String.Encoding.utf8) ?? "no data")\n---")
138-
let http = (sessResponse as? HTTPURLResponse) ?? HTTPURLResponse(url: request.url!, statusCode: 499, httpVersion: nil, headerFields: nil)!
139-
let response = OAuth2Response(data: sessData, request: request, response: http, error: error)
140-
callback(response)
121+
122+
do {
123+
// TODO: add support for aborting the request, see https://www.hackingwithswift.com/quick-start/concurrency/how-to-cancel-a-task
124+
let (sessData, sessResponse) = try await performer.perform(request: request)
125+
self.logger?.trace("OAuth2", msg: "RESPONSE\n\(sessResponse.debugDescription)\n\n\(String(data: sessData, encoding: String.Encoding.utf8) ?? "no data")\n---")
126+
127+
guard let response = sessResponse as? HTTPURLResponse else {
128+
throw CommonError.castError(
129+
from: String(describing: sessResponse.self),
130+
to: String(describing: HTTPURLResponse.self)
131+
)
132+
}
133+
134+
return OAuth2Response(data: sessData, request: request, response: response, error: nil)
135+
136+
} catch {
137+
self.logger?.trace("OAuth2", msg: "RESPONSE\nno response\n\nno data\n---")
138+
139+
let http = HTTPURLResponse(url: request.url!, statusCode: 499, httpVersion: nil, headerFields: nil)!
140+
return OAuth2Response(data: nil, request: request, response: http, error: error)
141141
}
142-
abortableTask = task
143142
}
144143

145144
/// Currently running abortable session task.
@@ -222,3 +221,16 @@ public func callOnMainThread(_ callback: (() -> Void)) {
222221
}
223222
}
224223

224+
// TODO: move to a separate file
225+
enum CommonError: Error {
226+
case castError(from: String, to: String)
227+
}
228+
229+
extension CommonError: CustomStringConvertible {
230+
public var description: String {
231+
switch self {
232+
case .castError(from: let from, to: let to):
233+
return "Could not cast \(from) to \(to)"
234+
}
235+
}
236+
}

Sources/Base/OAuth2Response.swift

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,14 @@ Encapsulates a URLResponse to a URLRequest.
2626

2727
Instances of this class are returned from `OAuth2Requestable` calls, they can be used like so:
2828

29-
perform(request: req) { response in
30-
do {
31-
let data = try response.responseData()
32-
// do what you must with `data` as Data and `response.response` as HTTPURLResponse
33-
}
34-
catch let error {
35-
// the request failed because of `error`
36-
}
37-
}
29+
await perform(request: req)
30+
do {
31+
let data = try response.responseData()
32+
// do what you must with `data` as Data and `response.response` as HTTPURLResponse
33+
}
34+
catch let error {
35+
// the request failed because of `error`
36+
}
3837
*/
3938
open class OAuth2Response {
4039

Sources/DataLoader/OAuth2DataLoader.swift

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ open class OAuth2DataLoader: OAuth2Requestable {
8080
- parameter request: The request to execute
8181
- parameter callback: The callback to call when the request completes/fails. Looks terrifying, see above on how to use it
8282
*/
83-
override open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) {
83+
open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) {
8484
perform(request: request, retry: true, callback: callback)
8585
}
8686

@@ -112,7 +112,9 @@ open class OAuth2DataLoader: OAuth2Requestable {
112112
return
113113
}
114114

115-
super.perform(request: request) { response in
115+
Task {
116+
let response = await super.perform(request: request)
117+
116118
do {
117119
if self.alsoIntercept403, 403 == response.response.statusCode {
118120
throw OAuth2Error.unauthorizedClient(nil)
@@ -126,16 +128,19 @@ open class OAuth2DataLoader: OAuth2Requestable {
126128
if retry {
127129
self.enqueue(request: request, callback: callback)
128130
self.oauth2.clientConfig.accessToken = nil
129-
self.attemptToAuthorize() { json, error in
130-
131-
// dequeue all if we're authorized, throw all away if something went wrong
132-
if nil != json {
133-
self.retryAll()
134-
}
135-
else {
136-
self.throwAllAway(with: error ?? OAuth2Error.requestCancelled)
131+
132+
133+
do {
134+
let json = try await self.attemptToAuthorize()
135+
guard json != nil else {
136+
throw OAuth2Error.requestCancelled
137137
}
138+
139+
self.retryAll()
140+
} catch {
141+
self.throwAllAway(with: error.asOAuth2Error)
138142
}
143+
139144
}
140145
else {
141146
callback(response)
@@ -157,14 +162,15 @@ open class OAuth2DataLoader: OAuth2Requestable {
157162
- parameter callback: The callback passed on from `authorize(callback:)`. Authorization finishes successfully (auth parameters will be
158163
non-nil but may be an empty dict), fails (error will be non-nil) or is canceled (both params and error are nil)
159164
*/
160-
open func attemptToAuthorize(callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) {
161-
if !isAuthorizing {
162-
isAuthorizing = true
163-
oauth2.authorize() { authParams, error in
164-
self.isAuthorizing = false
165-
callback(authParams, error)
166-
}
165+
open func attemptToAuthorize() async throws -> OAuth2JSON? {
166+
guard !self.isAuthorizing else {
167+
return nil
167168
}
169+
170+
self.isAuthorizing = true
171+
let authParams = try await oauth2.authorize()
172+
self.isAuthorizing = false
173+
return authParams
168174
}
169175

170176

0 commit comments

Comments
 (0)