Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Migrate from callbacks to async/await #21

Draft
wants to merge 3 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions OAuth2.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -953,7 +953,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 5.3.5;
METAL_ENABLE_DEBUG_INFO = YES;
Expand All @@ -962,7 +962,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 12.0;
TVOS_DEPLOYMENT_TARGET = 13.0;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
};
Expand Down Expand Up @@ -1010,14 +1010,14 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MACOSX_DEPLOYMENT_TARGET = 10.15;
MARKETING_VERSION = 5.3.5;
METAL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TVOS_DEPLOYMENT_TARGET = 12.0;
TVOS_DEPLOYMENT_TARGET = 13.0;
VALIDATE_PRODUCT = YES;
VERSIONING_SYSTEM = "apple-generic";
VERSION_INFO_PREFIX = "";
Expand Down
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import PackageDescription
let package = Package(
name: "OAuth2",
platforms: [
.macOS(.v10_15), .iOS(.v12), .tvOS(.v12), .watchOS(.v5)
.macOS(.v10_15), .iOS(.v13), .tvOS(.v13), .watchOS(.v6)
],
products: [
.library(name: "OAuth2", targets: ["OAuth2"]),
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ OAuth2 frameworks for **macOS**, **iOS** and **tvOS** written in Swift 5.
- [🖥 Sample macOS app][sample] (with data loader examples)
- [📖 Technical Documentation](https://p2.github.io/OAuth2)

OAuth2 requires Xcode 12.4, the built framework can be used on **OS X 10.15** or **iOS 12** and later.
OAuth2 requires Xcode 12.4, the built framework can be used on **OS X 10.15**, **iOS 13**, **tvOS** 13, **watchOS 6** and later.
Happy to accept pull requests, please see [CONTRIBUTING.md](./Docs/CONTRIBUTING.md)

### Swift Version
Expand Down
14 changes: 3 additions & 11 deletions Sources/Base/OAuth2Base.swift
Original file line number Diff line number Diff line change
Expand Up @@ -132,14 +132,8 @@ open class OAuth2Base: OAuth2Securable {
set { clientConfig.customUserAgent = newValue }
}


/// This closure is internally used with `authorize(params:callback:)` and only exposed for subclassing reason, do not mess with it!
public final var didAuthorizeOrFail: ((_ parameters: OAuth2JSON?, _ error: OAuth2Error?) -> Void)?

/// Returns true if the receiver is currently authorizing.
public final var isAuthorizing: Bool {
return nil != didAuthorizeOrFail
}
public final var isAuthorizing: Bool = false

/// Returns true if the receiver is currently exchanging the refresh token.
public final var isExchangingRefreshToken: Bool = false
Expand Down Expand Up @@ -277,8 +271,7 @@ open class OAuth2Base: OAuth2Securable {
storeTokensToKeychain()
}
callOnMainThread() {
self.didAuthorizeOrFail?(parameters, nil)
self.didAuthorizeOrFail = nil
self.isAuthorizing = false
self.internalAfterAuthorizeOrFail?(false, nil)
self.afterAuthorizeOrFail?(parameters, nil)
}
Expand All @@ -301,8 +294,7 @@ open class OAuth2Base: OAuth2Securable {
finalError = OAuth2Error.requestCancelled
}
callOnMainThread() {
self.didAuthorizeOrFail?(nil, finalError)
self.didAuthorizeOrFail = nil
self.isAuthorizing = false
self.internalAfterAuthorizeOrFail?(true, finalError)
self.afterAuthorizeOrFail?(nil, finalError)
}
Expand Down
22 changes: 7 additions & 15 deletions Sources/Base/OAuth2RequestPerformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,12 @@ The class `OAuth2DataTaskRequestPerformer` implements this protocol and is by de
public protocol OAuth2RequestPerformer {

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

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


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


/**
Designated initializer.
*/
Expand All @@ -45,18 +42,13 @@ open class OAuth2DataTaskRequestPerformer: OAuth2RequestPerformer {
}

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

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

64 changes: 38 additions & 26 deletions Sources/Base/OAuth2Requestable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,41 +105,40 @@ open class OAuth2Requestable {
open var requestPerformer: OAuth2RequestPerformer?

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

This implementation uses the shared `NSURLSession` and executes a data task. If the server responds with an error, this will be
converted into an error according to information supplied in the response JSON (if availale).

The callback returns a response object that is easy to use, like so:

perform(request: req) { response in
do {
let data = try response.responseData()
// do what you must with `data` as Data and `response.response` as HTTPURLResponse
}
catch let error {
// the request failed because of `error`
}
}

Easy, right?
This implementation uses the shared `NSURLSession`. If the server responds with an error, this will be
converted into an error according to information supplied in the response JSON (if available).

- parameter request: The request to execute
- parameter callback: The callback to call when the request completes/fails. Looks terrifying, see above on how to use it
- returns : OAuth2 response
*/
open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) {
open func perform(request: URLRequest) async -> OAuth2Response {
self.logger?.trace("OAuth2", msg: "REQUEST\n\(request.debugDescription)\n---")
let performer = requestPerformer ?? OAuth2DataTaskRequestPerformer(session: session)
requestPerformer = performer
let task = performer.perform(request: request) { sessData, sessResponse, error in
self.abortableTask = nil
self.logger?.trace("OAuth2", msg: "RESPONSE\n\(sessResponse?.debugDescription ?? "no response")\n\n\(String(data: sessData ?? Data(), encoding: String.Encoding.utf8) ?? "no data")\n---")
let http = (sessResponse as? HTTPURLResponse) ?? HTTPURLResponse(url: request.url!, statusCode: 499, httpVersion: nil, headerFields: nil)!
let response = OAuth2Response(data: sessData, request: request, response: http, error: error)
callback(response)

do {
// TODO: add support for aborting the request, see https://www.hackingwithswift.com/quick-start/concurrency/how-to-cancel-a-task
let (sessData, sessResponse) = try await performer.perform(request: request)
self.logger?.trace("OAuth2", msg: "RESPONSE\n\(sessResponse.debugDescription)\n\n\(String(data: sessData ?? Data(), encoding: String.Encoding.utf8) ?? "no data")\n---")

guard let response = sessResponse as? HTTPURLResponse else {
throw CommonError.castError(
from: String(describing: sessResponse.self),
to: String(describing: HTTPURLResponse.self)
)
}

return OAuth2Response(data: sessData, request: request, response: response, error: nil)

} catch {
self.logger?.trace("OAuth2", msg: "RESPONSE\nno response\n\nno data\n---")

let http = HTTPURLResponse(url: request.url!, statusCode: 499, httpVersion: nil, headerFields: nil)!
return OAuth2Response(data: nil, request: request, response: http, error: error)
}
abortableTask = task
}

/// Currently running abortable session task.
Expand Down Expand Up @@ -222,3 +221,16 @@ public func callOnMainThread(_ callback: (() -> Void)) {
}
}

// TODO: move to a separate file
enum CommonError: Error {
case castError(from: String, to: String)
}

extension CommonError: CustomStringConvertible {
public var description: String {
switch self {
case .castError(from: let from, to: let to):
return "Could not cast \(from) to \(to)"
}
}
}
17 changes: 8 additions & 9 deletions Sources/Base/OAuth2Response.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,14 @@ Encapsulates a URLResponse to a URLRequest.

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

perform(request: req) { response in
do {
let data = try response.responseData()
// do what you must with `data` as Data and `response.response` as HTTPURLResponse
}
catch let error {
// the request failed because of `error`
}
}
await perform(request: req)
do {
let data = try response.responseData()
// do what you must with `data` as Data and `response.response` as HTTPURLResponse
}
catch let error {
// the request failed because of `error`
}
*/
open class OAuth2Response {

Expand Down
40 changes: 23 additions & 17 deletions Sources/DataLoader/OAuth2DataLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ open class OAuth2DataLoader: OAuth2Requestable {
- parameter request: The request to execute
- parameter callback: The callback to call when the request completes/fails. Looks terrifying, see above on how to use it
*/
override open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) {
open func perform(request: URLRequest, callback: @escaping ((OAuth2Response) -> Void)) {
perform(request: request, retry: true, callback: callback)
}

Expand Down Expand Up @@ -112,7 +112,9 @@ open class OAuth2DataLoader: OAuth2Requestable {
return
}

super.perform(request: request) { response in
Task {
let response = await super.perform(request: request)

do {
if self.alsoIntercept403, 403 == response.response.statusCode {
throw OAuth2Error.unauthorizedClient(nil)
Expand All @@ -126,16 +128,19 @@ open class OAuth2DataLoader: OAuth2Requestable {
if retry {
self.enqueue(request: request, callback: callback)
self.oauth2.clientConfig.accessToken = nil
self.attemptToAuthorize() { json, error in

// dequeue all if we're authorized, throw all away if something went wrong
if nil != json {
self.retryAll()
}
else {
self.throwAllAway(with: error ?? OAuth2Error.requestCancelled)


do {
let json = try await self.attemptToAuthorize()
guard json != nil else {
throw OAuth2Error.requestCancelled
}

self.retryAll()
} catch {
self.throwAllAway(with: error.asOAuth2Error)
}

}
else {
callback(response)
Expand All @@ -157,14 +162,15 @@ open class OAuth2DataLoader: OAuth2Requestable {
- parameter callback: The callback passed on from `authorize(callback:)`. Authorization finishes successfully (auth parameters will be
non-nil but may be an empty dict), fails (error will be non-nil) or is canceled (both params and error are nil)
*/
open func attemptToAuthorize(callback: @escaping ((OAuth2JSON?, OAuth2Error?) -> Void)) {
if !isAuthorizing {
isAuthorizing = true
oauth2.authorize() { authParams, error in
self.isAuthorizing = false
callback(authParams, error)
}
open func attemptToAuthorize() async throws -> OAuth2JSON? {
guard !self.isAuthorizing else {
return nil
}

self.isAuthorizing = true
let authParams = try await oauth2.authorize()
self.isAuthorizing = false
return authParams
jozefizso marked this conversation as resolved.
Show resolved Hide resolved
}


Expand Down
Loading
Loading