Last active
July 11, 2025 06:32
-
-
Save sisoje/2e5e5f00b4f310d06245314b2b560376 to your computer and use it in GitHub Desktop.
The Composable Networking
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
import SwiftUI | |
// MARK: - errors | |
enum ComposableNetworkingError: Error { | |
case missingBearerToken | |
case missingRefreshToken | |
} | |
// MARK: - components | |
typealias NetworkingType = (URLRequest) async throws -> (Data, URLResponse) | |
struct AuthorizedNetworking { | |
@Binding var bearerToken: String? | |
let networking: NetworkingType | |
let authorizeRequest: ((URLRequest, String)) -> URLRequest // no default value | |
var composition: NetworkingType { | |
{ request in | |
guard let token = bearerToken else { | |
throw ComposableNetworkingError.missingBearerToken | |
} | |
return try await networking(authorizeRequest((request, token))) | |
} | |
} | |
} | |
struct ComposableNetworking { | |
@Binding var bearerToken: String? | |
let oauthNetworking: OAuthNetworking | |
let networking: NetworkingType | |
var composition: NetworkingType { | |
if bearerToken != nil { | |
oauthNetworking.composition | |
} else { | |
networking | |
} | |
} | |
} | |
struct OAuthNetworking { | |
@Binding var bearerToken: String? | |
let authorizedNetworking: AuthorizedNetworking | |
let localTokenRefresh: LocalTokenRefresh | |
let isNotAuthorized: (URLResponse) -> Bool | |
var composition: NetworkingType { | |
{ request in | |
let oldToken = bearerToken | |
let (data, response) = try await authorizedNetworking.composition(request) | |
guard isNotAuthorized(response) else { | |
return (data, response) | |
} | |
if oldToken == bearerToken { | |
try await localTokenRefresh.composition() | |
} | |
return try await authorizedNetworking.composition(request) | |
} | |
} | |
} | |
struct RemoteTokenRefresh { | |
let refreshTokenRequest: (String) -> URLRequest | |
let parseToken: (Data) throws -> String | |
let networking: NetworkingType | |
let isNotAuthorized: (URLResponse) -> Bool | |
var composition: (String) async throws -> String? { | |
{ refreshToken in | |
let req = refreshTokenRequest(refreshToken) | |
let (data, response) = try await networking(req) | |
if isNotAuthorized(response) { | |
return nil | |
} | |
return try parseToken(data) | |
} | |
} | |
} | |
struct LocalTokenRefresh { | |
@Binding var refreshToken: String? | |
@Binding var bearerToken: String? | |
@Binding var refreshTask: Task<Void, Error>? | |
let remoteTokenRefresh: RemoteTokenRefresh | |
var composition: () async throws -> Void { | |
{ | |
if let task = refreshTask { | |
return try await task.value | |
} | |
let task = Task { | |
guard let refreshToken = refreshToken else { | |
throw ComposableNetworkingError.missingRefreshToken | |
} | |
let token = try await remoteTokenRefresh.composition(refreshToken) | |
if let token { | |
self.bearerToken = token | |
} else { | |
self.bearerToken = nil | |
self.refreshToken = nil | |
} | |
} | |
refreshTask = task | |
defer { | |
refreshTask = nil | |
} | |
return try await task.value | |
} | |
} | |
} | |
// MARK: - composer | |
struct NetworkingComposer { | |
@Binding var bearerToken: String? | |
@Binding var refreshToken: String? | |
@Binding var refreshTask: Task<Void, Error>? | |
let networking: NetworkingType | |
let refreshTokenRequest: (String) -> URLRequest | |
let parseToken: (Data) throws -> String | |
var authorizeRequest: ((URLRequest, String)) -> URLRequest = { arg in | |
var req = arg.0 | |
req.setValue("Bearer \(arg.1)", forHTTPHeaderField: "Authorization") | |
return req | |
} | |
var isNotAuthorized: (URLResponse) -> Bool = { ($0 as? HTTPURLResponse)?.statusCode == 401 } | |
var remoteTokenRefresh: RemoteTokenRefresh { | |
RemoteTokenRefresh(refreshTokenRequest: refreshTokenRequest, parseToken: parseToken, networking: networking, isNotAuthorized: isNotAuthorized) | |
} | |
var authorizedNetworking: AuthorizedNetworking { | |
AuthorizedNetworking(bearerToken: $bearerToken, networking: networking, authorizeRequest: authorizeRequest) | |
} | |
var localTokenRefresh: LocalTokenRefresh { | |
LocalTokenRefresh(refreshToken: $refreshToken, bearerToken: $bearerToken, refreshTask: $refreshTask, remoteTokenRefresh: remoteTokenRefresh) | |
} | |
var oAuthNetworking: OAuthNetworking { | |
OAuthNetworking(bearerToken: $bearerToken, authorizedNetworking: authorizedNetworking, localTokenRefresh: localTokenRefresh, isNotAuthorized: isNotAuthorized) | |
} | |
var composableNetworking: ComposableNetworking { | |
ComposableNetworking(bearerToken: $bearerToken, oauthNetworking: oAuthNetworking, networking: networking) | |
} | |
var composition: NetworkingType { | |
composableNetworking.composition | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment