Skip to content

Commit

Permalink
Make the „response provider“ generic for the returned data type
Browse files Browse the repository at this point in the history
  • Loading branch information
fritzt0 committed Oct 28, 2024
1 parent 892dbb0 commit a27cb4e
Show file tree
Hide file tree
Showing 4 changed files with 66 additions and 56 deletions.
45 changes: 27 additions & 18 deletions Sources/OAuthenticator/Authenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public enum AuthenticatorError: Error {
}

/// Manage state required to executed authenticated URLRequests.
public actor Authenticator {
public actor Authenticator<UserDataType: Sendable> {
public typealias UserAuthenticator = @Sendable (URL, String) async throws -> URL
public typealias AuthenticationStatusHandler = (Result<Login, AuthenticatorError>) -> Void

Expand Down Expand Up @@ -85,32 +85,32 @@ public actor Authenticator {

let config: Configuration

let urlLoader: URLResponseProvider
let responseLoader: URLResponseProvider
let userDataLoader: URLUserDataProvider<UserDataType>
private var activeTokenTask: Task<Login, Error>?
private var localLogin: Login?

public init(config: Configuration, urlLoader loader: URLResponseProvider? = nil) {
public init(config: Configuration, responseLoader: URLResponseProvider? = nil, userDataLoader: @escaping URLUserDataProvider<UserDataType>) {
self.config = config

self.urlLoader = loader ?? URLSession.defaultProvider
self.responseLoader = responseLoader ?? URLSession.defaultProvider
self.userDataLoader = userDataLoader
}

/// A default `URLSession`-backed `URLResponseProvider`.
@available(*, deprecated, message: "Please move to URLSession.defaultProvider")
@MainActor
public static let defaultResponseProvider: URLResponseProvider = {
let session = URLSession(configuration: .default)
public init(config: Configuration, urlLoader: URLResponseProvider? = nil) where UserDataType == Data {
self.config = config

return session.responseProvider
}()
self.responseLoader = urlLoader ?? URLSession.defaultProvider
self.userDataLoader = urlLoader ?? URLSession.defaultProvider
}

/// Add authentication for `request`, execute it, and return its result.
public func response(for request: URLRequest) async throws -> (Data, URLResponse) {
public func response(for request: URLRequest) async throws -> (UserDataType, URLResponse) {
let userAuthenticator = config.userAuthenticator

let login = try await loginTaskResult(manual: false, userAuthenticator: userAuthenticator)

let result = try await authedResponse(for: request, login: login)
let result: (UserDataType, URLResponse) = try await authedResponse(for: request, login: login)

let action = try config.tokenHandling.responseStatusProvider(result)

Expand Down Expand Up @@ -146,13 +146,13 @@ public actor Authenticator {
}
}

private func authedResponse(for request: URLRequest, login: Login) async throws -> (Data, URLResponse) {
private func authedResponse(for request: URLRequest, login: Login) async throws -> (UserDataType, URLResponse) {
var authedRequest = request
let token = login.accessToken.value

authedRequest.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")

return try await urlLoader(authedRequest)
return try await userDataLoader(authedRequest)
}

/// Manually perform user authentication, if required.
Expand All @@ -161,6 +161,15 @@ public actor Authenticator {
}
}

/// A default `URLSession`-backed `URLResponseProvider`.
@available(*, deprecated, message: "Please move to URLSession.defaultProvider")
@MainActor
public let defaultAuthenticatorResponseProvider: URLResponseProvider = {
let session = URLSession(configuration: .default)

return session.responseProvider
}()

extension Authenticator {
private func retrieveLogin() async throws -> Login? {
guard let storage = config.loginStorage else {
Expand Down Expand Up @@ -256,7 +265,7 @@ extension Authenticator {
let scheme = try config.appCredentials.callbackURLScheme

let url = try await userAuthenticator(codeURL, scheme)
let login = try await config.tokenHandling.loginProvider(url, config.appCredentials, codeURL, urlLoader)
let login = try await config.tokenHandling.loginProvider(url, config.appCredentials, codeURL, responseLoader)

try await storeLogin(login)

Expand All @@ -276,7 +285,7 @@ extension Authenticator {
return nil
}

let login = try await refreshProvider(login, config.appCredentials, urlLoader)
let login = try await refreshProvider(login, config.appCredentials, responseLoader)

try await storeLogin(login)

Expand All @@ -285,7 +294,7 @@ extension Authenticator {
}

extension Authenticator {
public nonisolated var responseProvider: URLResponseProvider {
public nonisolated var responseProvider: URLUserDataProvider<UserDataType> {
{ try await self.response(for: $0) }
}
}
7 changes: 4 additions & 3 deletions Sources/OAuthenticator/Models.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Foundation
/// This is used to abstract the actual networking system from the underlying authentication
/// mechanism.
public typealias URLResponseProvider = @Sendable (URLRequest) async throws -> (Data, URLResponse)
public typealias URLUserDataProvider<UserDataType: Sendable> = @Sendable (URLRequest) async throws -> (UserDataType, URLResponse)

public struct Token: Codable, Hashable, Sendable {
public let value: String
Expand Down Expand Up @@ -97,7 +98,7 @@ public struct TokenHandling {
public typealias AuthorizationURLProvider = @Sendable (AppCredentials) throws -> URL
public typealias LoginProvider = @Sendable (URL, AppCredentials, URL, URLResponseProvider) async throws -> Login
public typealias RefreshProvider = @Sendable (Login, AppCredentials, URLResponseProvider) async throws -> Login
public typealias ResponseStatusProvider = @Sendable ((Data, URLResponse)) throws -> ResponseStatus
public typealias ResponseStatusProvider = @Sendable ((any Sendable, URLResponse)) throws -> ResponseStatus

public let authorizationURLProvider: AuthorizationURLProvider
public let loginProvider: LoginProvider
Expand All @@ -115,12 +116,12 @@ public struct TokenHandling {
}

@Sendable
public static func allResponsesValid(result: (Data, URLResponse)) throws -> ResponseStatus {
public static func allResponsesValid<UserDataType: Sendable>(result: (UserDataType, URLResponse)) throws -> ResponseStatus {
return .valid
}

@Sendable
public static func refreshOrAuthorizeWhenUnauthorized(result: (Data, URLResponse)) throws -> ResponseStatus {
public static func refreshOrAuthorizeWhenUnauthorized<UserDataType: Sendable>(result: (UserDataType, URLResponse)) throws -> ResponseStatus {
guard let response = result.1 as? HTTPURLResponse else {
throw AuthenticatorError.httpResponseExpected
}
Expand Down
62 changes: 31 additions & 31 deletions Tests/OAuthenticatorTests/AuthenticatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ final class AuthenticatorTests: XCTestCase {
storeTokenExp.fulfill()
}

let config = Authenticator.Configuration(
let config = Authenticator<Data>.Configuration(
appCredentials: Self.mockCredentials,
loginStorage: storage,
// loginStorage: nil,
Expand Down Expand Up @@ -148,10 +148,10 @@ final class AuthenticatorTests: XCTestCase {
XCTFail()
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 156 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 156 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 156 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Expand Down Expand Up @@ -200,10 +200,10 @@ final class AuthenticatorTests: XCTestCase {
XCTAssertEqual(login.accessToken.value, "REFRESHED")
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Check warning on line 208 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=macOS)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 208 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=iOS Simulator,name=iPhone 11)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Check warning on line 208 in Tests/OAuthenticatorTests/AuthenticatorTests.swift

View workflow job for this annotation

GitHub Actions / Test (platform=tvOS Simulator,name=Apple TV)

passing argument of non-sendable type 'Authenticator<Data>.Configuration' into actor-isolated context may introduce data races

Expand Down Expand Up @@ -235,10 +235,10 @@ final class AuthenticatorTests: XCTestCase {
return URL(string: "my://login")!
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator)

let loadExp = expectation(description: "load url")
let mockLoader: URLResponseProvider = { request in
Expand Down Expand Up @@ -301,11 +301,11 @@ final class AuthenticatorTests: XCTestCase {
}

// Configure Authenticator with result callback
let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator,
authenticationStatusHandler: authenticationCallback)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: mockUserAuthenticator,
authenticationStatusHandler: authenticationCallback)

let loadExp = expectation(description: "load url")
let mockLoader: URLResponseProvider = { request in
Expand Down Expand Up @@ -357,11 +357,11 @@ final class AuthenticatorTests: XCTestCase {
}

// Configure Authenticator with result callback
let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: Authenticator.failingUserAuthenticator,
authenticationStatusHandler: authenticationCallback)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
tokenHandling: tokenHandling,
mode: .manualOnly,
userAuthenticator: Authenticator<Data>.failingUserAuthenticator,
authenticationStatusHandler: authenticationCallback)

let auth = Authenticator(config: config, urlLoader: nil)
do {
Expand Down Expand Up @@ -408,10 +408,10 @@ final class AuthenticatorTests: XCTestCase {
XCTAssertEqual(login.accessToken.value, "REFRESHED")
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader.responseProvider)

Expand Down Expand Up @@ -468,10 +468,10 @@ final class AuthenticatorTests: XCTestCase {
savedLogins.append(login)
}

let config = Authenticator.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)
let config = Authenticator<Data>.Configuration(appCredentials: Self.mockCredentials,
loginStorage: storage,
tokenHandling: tokenHandling,
userAuthenticator: Self.disabledUserAuthenticator)

let auth = Authenticator(config: config, urlLoader: mockLoader)

Expand Down
8 changes: 4 additions & 4 deletions Tests/OAuthenticatorTests/GoogleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,10 +44,10 @@ final class GoogleTests: XCTestCase {

let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!)
let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters)
let config = Authenticator.Configuration(
let config = Authenticator<Data>.Configuration(
appCredentials: creds,
tokenHandling: tokenHandling,
userAuthenticator: Authenticator.failingUserAuthenticator
userAuthenticator: Authenticator<Data>.failingUserAuthenticator
)

// Validate URL is properly constructed
Expand Down Expand Up @@ -76,10 +76,10 @@ final class GoogleTests: XCTestCase {

let creds = AppCredentials(clientId: "client_id", clientPassword: "client_pwd", scopes: ["scope1", "scope2"], callbackURL: callback!)
let tokenHandling = GoogleAPI.googleAPITokenHandling(with: googleParameters)
let config = Authenticator.Configuration(
let config = Authenticator<Data>.Configuration(
appCredentials: creds,
tokenHandling: tokenHandling,
userAuthenticator: Authenticator.failingUserAuthenticator
userAuthenticator: Authenticator<Data>.failingUserAuthenticator
)

// Validate URL is properly constructed
Expand Down

0 comments on commit a27cb4e

Please sign in to comment.