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 27, 2024
1 parent 07b4e1e commit 860135f
Show file tree
Hide file tree
Showing 4 changed files with 68 additions and 58 deletions.
43 changes: 26 additions & 17 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 final class Authenticator {
public final class Authenticator<UserDataType: Sendable> {
public typealias UserAuthenticator = (URL, String) async throws -> URL
public typealias AuthenticationStatusHandler = (Result<Login, AuthenticatorError>) -> Void

Expand Down Expand Up @@ -81,31 +81,32 @@ public final class 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`.
@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 @@ -141,13 +142,13 @@ public final class 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 @@ -157,6 +158,14 @@ public final class Authenticator {
}
}

/// A default `URLSession`-backed `URLResponseProvider`.
@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 @@ -252,7 +261,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 @@ -272,7 +281,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 @@ -281,7 +290,7 @@ extension Authenticator {
}

extension Authenticator {
public var responseProvider: URLResponseProvider {
public 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 = (URLRequest) async throws -> (Data, URLResponse)
public typealias URLUserDataProvider<UserDataType: 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 = (AppCredentials) throws -> URL
public typealias LoginProvider = (URL, AppCredentials, URL, URLResponseProvider) async throws -> Login
public typealias RefreshProvider = (Login, AppCredentials, URLResponseProvider) async throws -> Login
public typealias ResponseStatusProvider = ((Data, URLResponse)) throws -> ResponseStatus
public typealias ResponseStatusProvider = ((any Sendable, URLResponse)) throws -> ResponseStatus

public let authorizationURLProvider: AuthorizationURLProvider
public let loginProvider: LoginProvider
Expand All @@ -114,11 +115,11 @@ public struct TokenHandling {
self.responseStatusProvider = responseStatusProvider
}

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

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
68 changes: 34 additions & 34 deletions Tests/OAuthenticatorTests/AuthenticatorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -96,10 +96,10 @@ final class AuthenticatorTests: XCTestCase {
storeTokenExp.fulfill()
}

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

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

Expand Down Expand Up @@ -132,10 +132,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)

Expand Down Expand Up @@ -184,10 +184,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)

Expand Down Expand Up @@ -219,10 +219,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 @@ -286,11 +286,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 @@ -343,11 +343,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 @@ -395,10 +395,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 @@ -453,10 +453,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 860135f

Please sign in to comment.