Skip to content

Commit 003bb94

Browse files
authored
Lots more cleaning up (#11)
* Update min SwiftHTTPTypes version * Fix readme link * Add missing dependency declaration * Lots of general code cleanup * Log better timing in NetworkClient * Improve locking usage in AuthState for TokenAuthenticationHandler * Tests respect LOG_LEVEL env
1 parent 93e19f9 commit 003bb94

16 files changed

+167
-186
lines changed

Package.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,29 @@ let swiftSettings: [SwiftSetting] = [
2424

2525
let package = Package(
2626
name: "StructuredAPIClient",
27+
platforms: [
28+
.macOS(.v10_15),
29+
.iOS(.v13),
30+
.watchOS(.v6),
31+
.tvOS(.v13),
32+
],
2733
products: [
2834
.library(name: "StructuredAPIClient", targets: ["StructuredAPIClient"]),
2935
.library(name: "StructuredAPIClientTestSupport", targets: ["StructuredAPIClientTestSupport"]),
3036
],
3137
dependencies: [
3238
// Swift logging API
3339
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
34-
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
40+
.package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.2"),
41+
.package(url: "https://github.com/stairtree/async-helpers.git", from: "0.2.0"),
3542
],
3643
targets: [
3744
.target(
3845
name: "StructuredAPIClient",
3946
dependencies: [
4047
.product(name: "Logging", package: "swift-log"),
4148
.product(name: "HTTPTypes", package: "swift-http-types"),
49+
.product(name: "AsyncHelpers", package: "async-helpers"),
4250
],
4351
swiftSettings: swiftSettings
4452
),

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</a>
1212

1313
<a href="https://codecov.io/gh/stairtree/StructuredAPIClient">
14-
<img src="https://img.shields.io/codecov/c/gh/stairtree/StructuredAPIClient?style=plastic&logo=codecov&label=codecov&token=MD69L97AOO">
14+
<img src="https://img.shields.io/codecov/c/gh/stairtree/StructuredAPIClient?style=plastic&logo=codecov&label=codecov">
1515
</a>
1616

1717
<a href="https://swift.org">

Sources/StructuredAPIClient/Handlers/AddHTTPHeadersHandler.swift

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,52 +16,58 @@ import Foundation
1616
import FoundationNetworking
1717
#endif
1818

19-
/// Add headers to an existing `Transport`.
19+
/// Add headers to all requests made by an existing ``Transport``.
2020
public final class AddHTTPHeadersHandler: Transport {
2121

2222
/// An enumeration of the possible modes for working with headers.
23-
public enum Mode: CaseIterable, Sendable {
24-
/// Accumulating behavior - if a given header is already specified by a request, the transport's value is
25-
/// appended, as per `URLRequest.addValue(_:forHTTPHeaderField:)`.
23+
public enum Mode: CaseIterable, Sendable, Hashable {
24+
/// Accumulating behavior - if a given header is already specified by a request, the transport's
25+
/// value is appended, as per `URLRequest.addValue(_:forHTTPHeaderField:)`.
2626
///
2727
/// - Warning: This is rarely what you want.
2828
case append
2929

30-
/// Overwriting behavior - if a given header is already specified by a request, the transport's value for the
31-
/// header replaces it, as per `URLRequest.setValue(_:forHTTPHeaderField:)`. In this mode, a request's header
32-
/// value is always overwritten.
30+
/// Overwriting behavior - if a given header is already specified by a request, the transport's
31+
/// value for the header replaces it, as per `URLRequest.setValue(_:forHTTPHeaderField:)`. In this
32+
/// mode, a request's header value is always overwritten.
3333
case replace
3434

35-
/// Polyfill behavior - if a given header is already specified by a request, it is left untouched, and the
36-
/// transport's value is ignored.
35+
/// Polyfill behavior - if a given header is already specified by a request, it is left untouched,
36+
/// and the transport's value is ignored.
3737
///
3838
/// This behavior is the default.
3939
case add
4040
}
4141

42-
/// The base `Transport` to extend with extra headers.
42+
/// The base ``Transport`` to extend with extra headers.
4343
///
4444
/// - Note: Never `nil` in practice for this transport.
4545
public let next: (any Transport)?
4646

4747
/// Additional headers that will be applied to the request upon sending.
4848
private let headers: [String: String]
4949

50-
/// The mode used to add additional headers. Defaults to `.append` for legacy compatibility.
50+
/// The mode used to add additional headers. Defaults to ``Mode/append`` for legacy compatibility.
5151
private let mode: Mode
5252

53-
/// Create a `Transport` that adds headers to the base `Transport`
53+
/// Create a ``Transport`` that adds headers to the given base ``Transport``
5454
/// - Parameters:
55-
/// - base: The base `Transport` that will have the headers applied
56-
/// - headers: Headers to apply to the base `Transport`
57-
/// - mode: The mode to use for resolving conflicts between a request's headers and the transport's headers.
55+
/// - base: The base ``Transport`` that will have the headers applied
56+
/// - headers: Headers to apply to the base ``Transport``
57+
/// - mode: The mode to use for resolving conflicts between a request's headers and the
58+
/// transport's headers.
5859
public init(base: any Transport, headers: [String: String], mode: Mode = .add) {
5960
self.next = base
6061
self.headers = headers
6162
self.mode = mode
6263
}
6364

65+
// See `Transport.send(request:completion:)`
6466
public func send(request: URLRequest, completion: @escaping @Sendable (Result<TransportResponse, any Error>) -> Void) {
67+
guard !self.headers.isEmpty else {
68+
return self.next?.send(request: request, completion: completion) ?? ()
69+
}
70+
6571
var newRequest = request
6672

6773
for (key, value) in self.headers {

Sources/StructuredAPIClient/Handlers/TokenAuthenticationHandler.swift

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ import Foundation
1616
@preconcurrency import FoundationNetworking
1717
#endif
1818
import Logging
19+
import AsyncHelpers
1920

20-
// Handle token auth and add appropriate auth headers to an existing transport.
21+
// Handle token auth and add appropriate auth headers to requests sent by an existing ``Transport``.
2122
public final class TokenAuthenticationHandler: Transport {
2223
public let next: (any Transport)?
2324
private let logger: Logger
@@ -58,58 +59,40 @@ public protocol Token: Sendable {
5859
}
5960

6061
final class AuthState: @unchecked Sendable {
61-
private final class LockedTokens: @unchecked Sendable {
62-
private let lock = NSLock()
63-
private var accessToken: (any Token)?
64-
private var refreshToken: (any Token)?
65-
66-
init(accessToken: (any Token)?, refreshToken: (any Token)?) {
67-
self.accessToken = accessToken
68-
self.refreshToken = refreshToken
69-
}
70-
71-
func withLock<R>(_ closure: @escaping @Sendable (inout (any Token)?, inout (any Token)?) throws -> R) rethrows -> R {
72-
try self.lock.withLock {
73-
try closure(&self.accessToken, &self.refreshToken)
74-
}
75-
}
76-
}
77-
78-
private let tokens: LockedTokens
62+
private let tokens: Locking.LockedValueBox<(accessToken: (any Token)?, refreshToken: (any Token)?)>
7963
let provider: any TokenProvider
8064
let logger: Logger
8165

8266
internal init(accessToken: (any Token)? = nil, refreshToken: (any Token)? = nil, provider: any TokenProvider, logger: Logger? = nil) {
83-
self.tokens = .init(accessToken: accessToken, refreshToken: refreshToken)
67+
self.tokens = .init((accessToken: accessToken, refreshToken: refreshToken))
8468
self.provider = provider
8569
self.logger = logger ?? Logger(label: "AuthState")
8670
}
8771

8872
func token(_ completion: @escaping @Sendable (Result<String, any Error>) -> Void) {
89-
if let raw = self.tokens.withLock({ token, _ in token.flatMap { ($0.expiresAt ?? Date.distantFuture) > Date() ? $0.raw : nil } }) {
73+
let (latestAccessToken, latestRefreshToken) = self.tokens.withLockedValue { ($0.accessToken, $0.refreshToken) }
74+
75+
if let raw = latestAccessToken.flatMap({ ($0.expiresAt ?? Date.distantFuture) > Date() ? $0.raw : nil }) {
9076
return completion(.success(raw))
91-
} else if let refresh = self.tokens.withLock({ _, token in token.flatMap { ($0.expiresAt ?? Date.distantFuture) > Date() ? $0 : nil } }) {
92-
logger.trace("Refreshing token")
77+
} else if let refresh = latestRefreshToken.flatMap({ ($0.expiresAt ?? Date.distantFuture) > Date() ? $0 : nil }) {
78+
self.logger.trace("Refreshing token")
9379
self.provider.refreshToken(withRefreshToken: refresh, completion: { result in
9480
switch result {
9581
case let .failure(error):
9682
return completion(.failure(error))
9783
case let .success(access):
98-
self.tokens.withLock { token, _ in token = access }
84+
self.tokens.withLockedValue { $0.accessToken = access }
9985
return completion(.success(access.raw))
10086
}
10187
})
10288
} else {
103-
logger.trace("Fetching initial tokens")
89+
self.logger.trace("Fetching initial tokens")
10490
self.provider.fetchToken(completion: { result in
10591
switch result {
10692
case let .failure(error):
10793
return completion(.failure(error))
10894
case let .success((access, refresh)):
109-
self.tokens.withLock { accessToken, refreshToken in
110-
accessToken = access
111-
refreshToken = refresh
112-
}
95+
self.tokens.withLockedValue { $0 = (accessToken: access, refreshToken: refresh) }
11396
return completion(.success(access.raw))
11497
}
11598
})

Sources/StructuredAPIClient/NetworkClient+AsyncAwait.swift

Lines changed: 0 additions & 29 deletions
This file was deleted.

Sources/StructuredAPIClient/NetworkClient.swift

Lines changed: 34 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,22 @@ public final class NetworkClient {
2929
self.logger = logger ?? Logger(label: "NetworkClient")
3030
}
3131

32-
/// Fetch any `NetworkRequest` type and return the response asynchronously.
32+
/// Fetch any ``NetworkRequest`` type and return the response asynchronously.
3333
public func load<Request: NetworkRequest>(_ req: Request, completion: @escaping @Sendable (Result<Request.ResponseDataType, any Error>) -> Void) {
34-
let start = DispatchTime.now()
34+
let start = Date()
3535
// Construct the URLRequest
3636
do {
3737
let logger = self.logger
38-
let urlRequest = try req.makeRequest(baseURL: baseURL)
39-
logger.trace("\(urlRequest.debugString)")
38+
let urlRequest = try req.makeRequest(baseURL: self.baseURL)
4039

4140
// Send it to the transport
42-
transport().send(request: urlRequest) { result in
43-
// TODO: Deliver a more accurate split of the different phases of the request
44-
defer { logger.trace("Request '\(urlRequest.debugString)' took \(String(format: "%.4f", (.now() - start).milliseconds))ms") }
41+
self.transport().send(request: urlRequest) { result in
42+
let middle = Date()
43+
logger.trace("Request '\(urlRequest.debugString)' received response in \(start.millisecondsBeforeNowFormatted)ms")
44+
defer {
45+
logger.trace("Request '\(urlRequest.debugString)' was processed in \(middle.millisecondsBeforeNowFormatted)ms")
46+
logger.trace("Request '\(urlRequest.debugString)' took a total of \(start.millisecondsBeforeNowFormatted)ms")
47+
}
4548

4649
completion(result.flatMap { resp in .init { try req.parseResponse(resp) } })
4750
}
@@ -51,22 +54,33 @@ public final class NetworkClient {
5154
}
5255
}
5356

54-
internal extension DispatchTime {
55-
static func -(lhs: Self, rhs: Self) -> Self {
56-
.init(uptimeNanoseconds: lhs.uptimeNanoseconds - rhs.uptimeNanoseconds)
57+
extension NetworkClient {
58+
/// Fetch any ``NetworkRequest`` type asynchronously and return the response.
59+
public func load<Request: NetworkRequest>(_ req: Request) async throws -> Request.ResponseDataType {
60+
try await withCheckedThrowingContinuation { continuation in
61+
self.load(req) {
62+
continuation.resume(with: $0)
63+
}
64+
}
5765
}
58-
59-
var milliseconds: Double {
60-
Double(self.uptimeNanoseconds) / 1_000_000
66+
}
67+
68+
private extension Date {
69+
var millisecondsBeforeNowFormatted: String {
70+
#if canImport(Darwin)
71+
if #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) {
72+
return (self.timeIntervalSinceNow * -1000.0).formatted(.number.precision(.fractionLength(4...4)).grouping(.never))
73+
}
74+
#endif
75+
76+
// This is both easier and much faster than using NumberFormatter
77+
let msInterval = (self.timeIntervalSinceNow * -10_000_000.0).rounded(.toNearestOrEven) / 10_000.0
78+
return "\(Int(msInterval))\("\(msInterval)0000".drop(while: { $0 != "." }).prefix(5))"
6179
}
6280
}
6381

64-
#if !canImport(Darwin)
65-
extension NSLocking {
66-
package func withLock<R>(_ body: @Sendable () throws -> R) rethrows -> R {
67-
self.lock()
68-
defer { self.unlock() }
69-
return try body()
82+
internal extension URLRequest {
83+
var debugString: String {
84+
"\(self.httpMethod.map { "[\($0)] " } ?? "")\(url.map { "\($0) " } ?? "")"
7085
}
7186
}
72-
#endif

Sources/StructuredAPIClient/NetworkRequest.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,20 @@ import FoundationNetworking
1717
#endif
1818
import HTTPTypes
1919

20-
/// Any request that can be sent as a `URLRequest` with a `NetworkClient`, and returns a response.
20+
/// Any request that can be sent as a `URLRequest` with a ``NetworkClient``, and returns a response.
2121
public protocol NetworkRequest: Sendable {
2222
/// The decoded data type that represents the response.
2323
associatedtype ResponseDataType: Sendable
2424

2525
/// Returns a request based on the given base URL.
26-
/// - Parameter baseURL: The `NetworkClient`'s base URL.
26+
/// - Parameter baseURL: The ``NetworkClient``'s base URL.
2727
func makeRequest(baseURL: URL) throws -> URLRequest
2828

29-
/// Processes a response returned from the transport and either returns the associated response type or throws an
30-
/// application-specific error.
29+
/// Processes a response returned from the transport and either returns the associated response type
30+
/// or throws an application-specific error.
3131
///
3232
/// - Parameters:
33-
/// - response: A `TransportResponse` containing the response to a request.
33+
/// - response: A ``TransportResponse`` containing the response to a request.
3434
func parseResponse(_ response: TransportResponse) throws -> ResponseDataType
3535
}
3636

0 commit comments

Comments
 (0)