diff --git a/Package.swift b/Package.swift
index 46d0e2f..32d8233 100644
--- a/Package.swift
+++ b/Package.swift
@@ -24,6 +24,12 @@ let swiftSettings: [SwiftSetting] = [
let package = Package(
name: "StructuredAPIClient",
+ platforms: [
+ .macOS(.v10_15),
+ .iOS(.v13),
+ .watchOS(.v6),
+ .tvOS(.v13),
+ ],
products: [
.library(name: "StructuredAPIClient", targets: ["StructuredAPIClient"]),
.library(name: "StructuredAPIClientTestSupport", targets: ["StructuredAPIClientTestSupport"]),
@@ -31,7 +37,8 @@ let package = Package(
dependencies: [
// Swift logging API
.package(url: "https://github.com/apple/swift-log.git", from: "1.5.0"),
- .package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.0"),
+ .package(url: "https://github.com/apple/swift-http-types.git", from: "1.0.2"),
+ .package(url: "https://github.com/stairtree/async-helpers.git", from: "0.2.0"),
],
targets: [
.target(
@@ -39,6 +46,7 @@ let package = Package(
dependencies: [
.product(name: "Logging", package: "swift-log"),
.product(name: "HTTPTypes", package: "swift-http-types"),
+ .product(name: "AsyncHelpers", package: "async-helpers"),
],
swiftSettings: swiftSettings
),
diff --git a/README.md b/README.md
index 55ce18d..fa34ce2 100644
--- a/README.md
+++ b/README.md
@@ -11,7 +11,7 @@
-
+
diff --git a/Sources/StructuredAPIClient/Handlers/AddHTTPHeadersHandler.swift b/Sources/StructuredAPIClient/Handlers/AddHTTPHeadersHandler.swift
index a1df7f5..06036aa 100644
--- a/Sources/StructuredAPIClient/Handlers/AddHTTPHeadersHandler.swift
+++ b/Sources/StructuredAPIClient/Handlers/AddHTTPHeadersHandler.swift
@@ -16,30 +16,30 @@ import Foundation
import FoundationNetworking
#endif
-/// Add headers to an existing `Transport`.
+/// Add headers to all requests made by an existing ``Transport``.
public final class AddHTTPHeadersHandler: Transport {
/// An enumeration of the possible modes for working with headers.
- public enum Mode: CaseIterable, Sendable {
- /// Accumulating behavior - if a given header is already specified by a request, the transport's value is
- /// appended, as per `URLRequest.addValue(_:forHTTPHeaderField:)`.
+ public enum Mode: CaseIterable, Sendable, Hashable {
+ /// Accumulating behavior - if a given header is already specified by a request, the transport's
+ /// value is appended, as per `URLRequest.addValue(_:forHTTPHeaderField:)`.
///
/// - Warning: This is rarely what you want.
case append
- /// Overwriting behavior - if a given header is already specified by a request, the transport's value for the
- /// header replaces it, as per `URLRequest.setValue(_:forHTTPHeaderField:)`. In this mode, a request's header
- /// value is always overwritten.
+ /// Overwriting behavior - if a given header is already specified by a request, the transport's
+ /// value for the header replaces it, as per `URLRequest.setValue(_:forHTTPHeaderField:)`. In this
+ /// mode, a request's header value is always overwritten.
case replace
- /// Polyfill behavior - if a given header is already specified by a request, it is left untouched, and the
- /// transport's value is ignored.
+ /// Polyfill behavior - if a given header is already specified by a request, it is left untouched,
+ /// and the transport's value is ignored.
///
/// This behavior is the default.
case add
}
- /// The base `Transport` to extend with extra headers.
+ /// The base ``Transport`` to extend with extra headers.
///
/// - Note: Never `nil` in practice for this transport.
public let next: (any Transport)?
@@ -47,21 +47,27 @@ public final class AddHTTPHeadersHandler: Transport {
/// Additional headers that will be applied to the request upon sending.
private let headers: [String: String]
- /// The mode used to add additional headers. Defaults to `.append` for legacy compatibility.
+ /// The mode used to add additional headers. Defaults to ``Mode/append`` for legacy compatibility.
private let mode: Mode
- /// Create a `Transport` that adds headers to the base `Transport`
+ /// Create a ``Transport`` that adds headers to the given base ``Transport``
/// - Parameters:
- /// - base: The base `Transport` that will have the headers applied
- /// - headers: Headers to apply to the base `Transport`
- /// - mode: The mode to use for resolving conflicts between a request's headers and the transport's headers.
+ /// - base: The base ``Transport`` that will have the headers applied
+ /// - headers: Headers to apply to the base ``Transport``
+ /// - mode: The mode to use for resolving conflicts between a request's headers and the
+ /// transport's headers.
public init(base: any Transport, headers: [String: String], mode: Mode = .add) {
self.next = base
self.headers = headers
self.mode = mode
}
+ // See `Transport.send(request:completion:)`
public func send(request: URLRequest, completion: @escaping @Sendable (Result) -> Void) {
+ guard !self.headers.isEmpty else {
+ return self.next?.send(request: request, completion: completion) ?? ()
+ }
+
var newRequest = request
for (key, value) in self.headers {
diff --git a/Sources/StructuredAPIClient/Handlers/TokenAuthenticationHandler.swift b/Sources/StructuredAPIClient/Handlers/TokenAuthenticationHandler.swift
index 595ad36..e68324c 100644
--- a/Sources/StructuredAPIClient/Handlers/TokenAuthenticationHandler.swift
+++ b/Sources/StructuredAPIClient/Handlers/TokenAuthenticationHandler.swift
@@ -16,8 +16,9 @@ import Foundation
@preconcurrency import FoundationNetworking
#endif
import Logging
+import AsyncHelpers
-// Handle token auth and add appropriate auth headers to an existing transport.
+// Handle token auth and add appropriate auth headers to requests sent by an existing ``Transport``.
public final class TokenAuthenticationHandler: Transport {
public let next: (any Transport)?
private let logger: Logger
@@ -58,58 +59,40 @@ public protocol Token: Sendable {
}
final class AuthState: @unchecked Sendable {
- private final class LockedTokens: @unchecked Sendable {
- private let lock = NSLock()
- private var accessToken: (any Token)?
- private var refreshToken: (any Token)?
-
- init(accessToken: (any Token)?, refreshToken: (any Token)?) {
- self.accessToken = accessToken
- self.refreshToken = refreshToken
- }
-
- func withLock(_ closure: @escaping @Sendable (inout (any Token)?, inout (any Token)?) throws -> R) rethrows -> R {
- try self.lock.withLock {
- try closure(&self.accessToken, &self.refreshToken)
- }
- }
- }
-
- private let tokens: LockedTokens
+ private let tokens: Locking.LockedValueBox<(accessToken: (any Token)?, refreshToken: (any Token)?)>
let provider: any TokenProvider
let logger: Logger
internal init(accessToken: (any Token)? = nil, refreshToken: (any Token)? = nil, provider: any TokenProvider, logger: Logger? = nil) {
- self.tokens = .init(accessToken: accessToken, refreshToken: refreshToken)
+ self.tokens = .init((accessToken: accessToken, refreshToken: refreshToken))
self.provider = provider
self.logger = logger ?? Logger(label: "AuthState")
}
func token(_ completion: @escaping @Sendable (Result) -> Void) {
- if let raw = self.tokens.withLock({ token, _ in token.flatMap { ($0.expiresAt ?? Date.distantFuture) > Date() ? $0.raw : nil } }) {
+ let (latestAccessToken, latestRefreshToken) = self.tokens.withLockedValue { ($0.accessToken, $0.refreshToken) }
+
+ if let raw = latestAccessToken.flatMap({ ($0.expiresAt ?? Date.distantFuture) > Date() ? $0.raw : nil }) {
return completion(.success(raw))
- } else if let refresh = self.tokens.withLock({ _, token in token.flatMap { ($0.expiresAt ?? Date.distantFuture) > Date() ? $0 : nil } }) {
- logger.trace("Refreshing token")
+ } else if let refresh = latestRefreshToken.flatMap({ ($0.expiresAt ?? Date.distantFuture) > Date() ? $0 : nil }) {
+ self.logger.trace("Refreshing token")
self.provider.refreshToken(withRefreshToken: refresh, completion: { result in
switch result {
case let .failure(error):
return completion(.failure(error))
case let .success(access):
- self.tokens.withLock { token, _ in token = access }
+ self.tokens.withLockedValue { $0.accessToken = access }
return completion(.success(access.raw))
}
})
} else {
- logger.trace("Fetching initial tokens")
+ self.logger.trace("Fetching initial tokens")
self.provider.fetchToken(completion: { result in
switch result {
case let .failure(error):
return completion(.failure(error))
case let .success((access, refresh)):
- self.tokens.withLock { accessToken, refreshToken in
- accessToken = access
- refreshToken = refresh
- }
+ self.tokens.withLockedValue { $0 = (accessToken: access, refreshToken: refresh) }
return completion(.success(access.raw))
}
})
diff --git a/Sources/StructuredAPIClient/NetworkClient+AsyncAwait.swift b/Sources/StructuredAPIClient/NetworkClient+AsyncAwait.swift
deleted file mode 100644
index edcf30e..0000000
--- a/Sources/StructuredAPIClient/NetworkClient+AsyncAwait.swift
+++ /dev/null
@@ -1,29 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the StructuredAPIClient open source project
-//
-// Copyright (c) Stairtree GmbH
-// Licensed under the MIT license
-//
-// See LICENSE.txt for license information
-//
-// SPDX-License-Identifier: MIT
-//
-//===----------------------------------------------------------------------===//
-
-import Foundation
-#if canImport(FoundationNetworking)
-import FoundationNetworking
-#endif
-
-@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
-extension NetworkClient {
- public func load(_ req: Request) async throws -> Request.ResponseDataType {
- try await withCheckedThrowingContinuation { continuation in
- self.load(req) { switch $0 {
- case .success(let value): continuation.resume(returning: value)
- case .failure(let error): continuation.resume(throwing: error)
- } }
- }
- }
-}
diff --git a/Sources/StructuredAPIClient/NetworkClient.swift b/Sources/StructuredAPIClient/NetworkClient.swift
index 6b3f462..4f93ef5 100644
--- a/Sources/StructuredAPIClient/NetworkClient.swift
+++ b/Sources/StructuredAPIClient/NetworkClient.swift
@@ -29,19 +29,22 @@ public final class NetworkClient {
self.logger = logger ?? Logger(label: "NetworkClient")
}
- /// Fetch any `NetworkRequest` type and return the response asynchronously.
+ /// Fetch any ``NetworkRequest`` type and return the response asynchronously.
public func load(_ req: Request, completion: @escaping @Sendable (Result) -> Void) {
- let start = DispatchTime.now()
+ let start = Date()
// Construct the URLRequest
do {
let logger = self.logger
- let urlRequest = try req.makeRequest(baseURL: baseURL)
- logger.trace("\(urlRequest.debugString)")
+ let urlRequest = try req.makeRequest(baseURL: self.baseURL)
// Send it to the transport
- transport().send(request: urlRequest) { result in
- // TODO: Deliver a more accurate split of the different phases of the request
- defer { logger.trace("Request '\(urlRequest.debugString)' took \(String(format: "%.4f", (.now() - start).milliseconds))ms") }
+ self.transport().send(request: urlRequest) { result in
+ let middle = Date()
+ logger.trace("Request '\(urlRequest.debugString)' received response in \(start.millisecondsBeforeNowFormatted)ms")
+ defer {
+ logger.trace("Request '\(urlRequest.debugString)' was processed in \(middle.millisecondsBeforeNowFormatted)ms")
+ logger.trace("Request '\(urlRequest.debugString)' took a total of \(start.millisecondsBeforeNowFormatted)ms")
+ }
completion(result.flatMap { resp in .init { try req.parseResponse(resp) } })
}
@@ -51,22 +54,33 @@ public final class NetworkClient {
}
}
-internal extension DispatchTime {
- static func -(lhs: Self, rhs: Self) -> Self {
- .init(uptimeNanoseconds: lhs.uptimeNanoseconds - rhs.uptimeNanoseconds)
+extension NetworkClient {
+ /// Fetch any ``NetworkRequest`` type asynchronously and return the response.
+ public func load(_ req: Request) async throws -> Request.ResponseDataType {
+ try await withCheckedThrowingContinuation { continuation in
+ self.load(req) {
+ continuation.resume(with: $0)
+ }
+ }
}
-
- var milliseconds: Double {
- Double(self.uptimeNanoseconds) / 1_000_000
+}
+
+private extension Date {
+ var millisecondsBeforeNowFormatted: String {
+ #if canImport(Darwin)
+ if #available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *) {
+ return (self.timeIntervalSinceNow * -1000.0).formatted(.number.precision(.fractionLength(4...4)).grouping(.never))
+ }
+ #endif
+
+ // This is both easier and much faster than using NumberFormatter
+ let msInterval = (self.timeIntervalSinceNow * -10_000_000.0).rounded(.toNearestOrEven) / 10_000.0
+ return "\(Int(msInterval))\("\(msInterval)0000".drop(while: { $0 != "." }).prefix(5))"
}
}
-#if !canImport(Darwin)
-extension NSLocking {
- package func withLock(_ body: @Sendable () throws -> R) rethrows -> R {
- self.lock()
- defer { self.unlock() }
- return try body()
+internal extension URLRequest {
+ var debugString: String {
+ "\(self.httpMethod.map { "[\($0)] " } ?? "")\(url.map { "\($0) " } ?? "")"
}
}
-#endif
diff --git a/Sources/StructuredAPIClient/NetworkRequest.swift b/Sources/StructuredAPIClient/NetworkRequest.swift
index 3c8a67d..ad6918f 100644
--- a/Sources/StructuredAPIClient/NetworkRequest.swift
+++ b/Sources/StructuredAPIClient/NetworkRequest.swift
@@ -17,20 +17,20 @@ import FoundationNetworking
#endif
import HTTPTypes
-/// Any request that can be sent as a `URLRequest` with a `NetworkClient`, and returns a response.
+/// Any request that can be sent as a `URLRequest` with a ``NetworkClient``, and returns a response.
public protocol NetworkRequest: Sendable {
/// The decoded data type that represents the response.
associatedtype ResponseDataType: Sendable
/// Returns a request based on the given base URL.
- /// - Parameter baseURL: The `NetworkClient`'s base URL.
+ /// - Parameter baseURL: The ``NetworkClient``'s base URL.
func makeRequest(baseURL: URL) throws -> URLRequest
- /// Processes a response returned from the transport and either returns the associated response type or throws an
- /// application-specific error.
+ /// Processes a response returned from the transport and either returns the associated response type
+ /// or throws an application-specific error.
///
/// - Parameters:
- /// - response: A `TransportResponse` containing the response to a request.
+ /// - response: A ``TransportResponse`` containing the response to a request.
func parseResponse(_ response: TransportResponse) throws -> ResponseDataType
}
diff --git a/Sources/StructuredAPIClient/Transport/Transport.swift b/Sources/StructuredAPIClient/Transport/Transport.swift
index 881e71e..1636e21 100644
--- a/Sources/StructuredAPIClient/Transport/Transport.swift
+++ b/Sources/StructuredAPIClient/Transport/Transport.swift
@@ -17,7 +17,7 @@ import FoundationNetworking
#endif
import HTTPTypes
-/// A successful response from a `Transport`.
+/// A successful response from a ``Transport``.
public struct TransportResponse: Sendable {
/// The received HTTP status code.
public let status: HTTPResponse.Status
@@ -28,7 +28,7 @@ public struct TransportResponse: Sendable {
/// The raw HTTP response body. If there was no response body, this will have a zero length.
public let body: Data
- /// Create a new `TransportResponse`. Intended for use by `Transport` implementations.
+ /// Create a new ``TransportResponse``. Intended for use by ``Transport`` implementations.
public init(status: HTTPResponse.Status, headers: HTTPFields, body: Data) {
self.status = status
self.headers = headers
@@ -36,12 +36,13 @@ public struct TransportResponse: Sendable {
}
}
-/// A `Transport` maps a `URLRequest` to a `Status` and `Data` pair asynchronously.
+/// A ``Transport`` asynchronously maps a `URLRequest` to a ``TransportResponse``.
public protocol Transport: Sendable {
/// Sends the request and delivers the response asynchronously to a completion handler.
///
- /// Transports should make an effort to provide the most specific errors possible for failures. In particular, the
- /// `TransportFailure` enumeration is intended to encapsulate some of the most common failure modes.
+ /// Transports should make an effort to provide the most specific errors possible for failures.
+ /// In particular, the ``TransportFailure`` enumeration is intended to encapsulate some of the
+ /// most common failure modes.
///
/// - Parameters:
/// - request: The request to be sent.
@@ -49,20 +50,42 @@ public protocol Transport: Sendable {
/// - response: The received response from the server, or an error indicating a transport-level failure.
func send(request: URLRequest, completion: @escaping @Sendable (_ result: Result) -> Void)
- /// The next Transport that the request is being forwarded to.
+ /// The next ``Transport`` that the request is being forwarded to.
///
- /// If `nil`, this should be the final `Transport`.
+ /// If `nil`, this ``Transport`` is the end of the chain.
var next: (any Transport)? { get }
/// Cancel the request.
///
- /// - Note: Any `Tranport` forwarding the request must call `cancel()` on the next `Transport`.
+ /// - Note: Any ``Tranport`` forwarding the request must call `cancel()` on the next ``Transport``.
func cancel()
}
extension Transport {
- /// If there is no special handling of cancellation, the default implementation just forwards to the next `Transport`.
+ /// If there is no special handling of cancellation, the default implementation just forwards to
+ /// the next ``Transport``.
///
- /// - Note: You must call `cancel()` on the next `Transport` if you customize this method.
- public func cancel() { next?.cancel() }
+ /// - Note: Implementations which override this method must ensure that they forward the
+ /// cancellation to the next ``Transport`` in the chain, if any.
+ public func cancel() { self.next?.cancel() }
+}
+
+extension Transport {
+ /// Sends the request asynchronously and returns the response.
+ ///
+ /// Transports should make an effort to provide the most specific errors possible for failures.
+ /// In particular, the ``TransportFailure`` enumeration is intended to encapsulate some of the
+ /// most common failure modes.
+ ///
+ /// - Parameters:
+ /// - request: The request to be sent.
+ /// - Returns: The received response from the server.
+ /// - Throws: An error indicating a transport-level failure.
+ public func send(request: URLRequest) async throws -> TransportResponse {
+ try await withCheckedThrowingContinuation { continuation in
+ self.send(request: request, completion: {
+ continuation.resume(with: $0)
+ })
+ }
+ }
}
diff --git a/Sources/StructuredAPIClient/Transport/TransportFailure.swift b/Sources/StructuredAPIClient/Transport/TransportFailure.swift
index 55d4dbb..4cb6005 100644
--- a/Sources/StructuredAPIClient/Transport/TransportFailure.swift
+++ b/Sources/StructuredAPIClient/Transport/TransportFailure.swift
@@ -25,13 +25,18 @@ public enum TransportFailure: Error, Equatable {
case cancelled
case unknown(any Error)
- public static func ==(lhs: Self, rhs: Self) -> Bool {
+ public static func == (lhs: Self, rhs: Self) -> Bool {
switch (lhs, rhs) {
- case let (.invalidRequest(lurl, lcomp), .invalidRequest(rurl, rcomp)): return lurl == rurl && lcomp == rcomp
- case let (.network(lerror), .network(rerror)): return lerror == rerror
- case (.cancelled, .cancelled): return true
- case let (.unknown(lerror), .unknown(rerror)): return (lerror as NSError) == (rerror as NSError)
- default: return false
+ case let (.invalidRequest(lurl, lcomp), .invalidRequest(rurl, rcomp)):
+ lurl == rurl && lcomp == rcomp
+ case let (.network(lerror), .network(rerror)):
+ lerror == rerror
+ case (.cancelled, .cancelled):
+ true
+ case let (.unknown(lerror), .unknown(rerror)):
+ (lerror as NSError) == (rerror as NSError)
+ default:
+ false
}
}
}
diff --git a/Sources/StructuredAPIClient/Transport/URLSessionTransport+AsyncAwait.swift b/Sources/StructuredAPIClient/Transport/URLSessionTransport+AsyncAwait.swift
deleted file mode 100644
index 25be42e..0000000
--- a/Sources/StructuredAPIClient/Transport/URLSessionTransport+AsyncAwait.swift
+++ /dev/null
@@ -1,44 +0,0 @@
-//===----------------------------------------------------------------------===//
-//
-// This source file is part of the StructuredAPIClient open source project
-//
-// Copyright (c) Stairtree GmbH
-// Licensed under the MIT license
-//
-// See LICENSE.txt for license information
-//
-// SPDX-License-Identifier: MIT
-//
-//===----------------------------------------------------------------------===//
-
-import Foundation
-#if canImport(FoundationNetworking)
-import FoundationNetworking
-#endif
-
-#if canImport(Darwin)
-
-@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
-extension URLSessionTransport {
-
- /// Sends the request using a `URLSessionDataTask`
- /// - Parameter request: The configured request to send.
- /// - Returns: The received response from the server.
- public func send(request: URLRequest) async throws -> TransportResponse {
- do {
- let (data, response) = try await session.data(for: request)
- guard let httpResponse = response as? HTTPURLResponse else {
- throw TransportFailure.network(URLError(.unsupportedURL))
- }
- return httpResponse.asTransportResponse(withData: data)
- } catch let netError as URLError {
- throw netError.asTransportFailure
- } catch let error as TransportFailure {
- throw error
- } catch {
- throw TransportFailure.unknown(error)
- }
- }
-}
-
-#endif
diff --git a/Sources/StructuredAPIClient/Transport/URLSessionTransport.swift b/Sources/StructuredAPIClient/Transport/URLSessionTransport.swift
index 569029c..47dfb3b 100644
--- a/Sources/StructuredAPIClient/Transport/URLSessionTransport.swift
+++ b/Sources/StructuredAPIClient/Transport/URLSessionTransport.swift
@@ -16,16 +16,17 @@ import Foundation
@preconcurrency import FoundationNetworking
#endif
import HTTPTypes
+import AsyncHelpers
public final class URLSessionTransport: Transport {
/// The actual `URLSession` instance used to create request tasks.
public let session: URLSession
- /// See `Transport.next`.
+ // See `Transport.next`.
public var next: (any Transport)? { nil }
private final class LockedURLSessionDataTask: @unchecked Sendable {
- let lock = NSLock()
+ let lock = Locking.FastLock()
var task: URLSessionDataTask?
func setAndResume(_ newTask: URLSessionDataTask) {
@@ -57,7 +58,7 @@ public final class URLSessionTransport: Transport {
/// - completion: The completion handler that is called after the response is received.
/// - response: The received response from the server.
public func send(request: URLRequest, completion: @escaping @Sendable (Result) -> Void) {
- self.task.setAndResume(session.dataTask(with: request) { (data, response, error) in
+ self.task.setAndResume(self.session.dataTask(with: request) { (data, response, error) in
if let error {
return completion(.failure((error as? URLError)?.asTransportFailure ?? .unknown(error)))
}
@@ -81,26 +82,18 @@ public final class URLSessionTransport: Transport {
extension URLError {
var asTransportFailure: TransportFailure {
switch self.code {
- case .cancelled: .cancelled
- default: .network(self)
+ case .cancelled: .cancelled
+ default: .network(self)
}
}
}
-extension URLRequest {
- var debugString: String {
- "\(httpMethod.map { "[\($0)] " } ?? "")\(url.map { "\($0) " } ?? "")"
- }
-}
-
extension HTTPURLResponse {
func asTransportResponse(withData data: Data?) -> TransportResponse {
TransportResponse(
status: HTTPResponse.Status(code: self.statusCode),
headers: HTTPFields(self.allHeaderFields.compactMap { k, v in
- guard let name = (k.base as? String).flatMap(HTTPField.Name.init(_:)),
- let value = v as? String
- else { return nil }
+ guard let name = (k.base as? String).flatMap(HTTPField.Name.init(_:)), let value = v as? String else { return nil }
return HTTPField(name: name, value: value)
}),
diff --git a/Sources/StructuredAPIClientTestSupport/TestRequest.swift b/Sources/StructuredAPIClientTestSupport/TestRequest.swift
index f4295f5..613192f 100644
--- a/Sources/StructuredAPIClientTestSupport/TestRequest.swift
+++ b/Sources/StructuredAPIClientTestSupport/TestRequest.swift
@@ -17,8 +17,8 @@ import FoundationNetworking
#endif
import StructuredAPIClient
-/// A `NetworkRequest` that simply requests the base URL (optionally with additional headers added) and expects UTF-8
-/// `String`s as responses.
+/// A ``NetworkRequest`` that simply requests the base URL (optionally with additional headers added)
+/// and expects UTF-8 `String`s as responses.
public struct TestRequest: NetworkRequest {
private let extraHeaders: [String: String]
@@ -27,7 +27,7 @@ public struct TestRequest: NetworkRequest {
}
public func parseResponse(_ response: TransportResponse) throws -> String {
- return String(decoding: response.body, as: UTF8.self)
+ String(decoding: response.body, as: UTF8.self)
}
public func makeRequest(baseURL: URL) throws -> URLRequest {
diff --git a/Sources/StructuredAPIClientTestSupport/TestTokenProvider.swift b/Sources/StructuredAPIClientTestSupport/TestTokenProvider.swift
index 297d4da..3b281cc 100644
--- a/Sources/StructuredAPIClientTestSupport/TestTokenProvider.swift
+++ b/Sources/StructuredAPIClientTestSupport/TestTokenProvider.swift
@@ -17,8 +17,7 @@ import FoundationNetworking
#endif
import StructuredAPIClient
-
-/// A `TokenProvider` that returns a given accessToken and refreshToken for the respective requests.
+/// A ``TokenProvider`` that returns a given access token and refresh token for the respective requests.
public final class TestTokenProvider: TokenProvider, Sendable {
let accessToken: any Token
let refreshToken: any Token
@@ -37,10 +36,11 @@ public final class TestTokenProvider: TokenProvider, Sendable {
}
}
-/// A sample `Token` that contains the raw String and an expiry date.
+/// A sample ``Token`` that contains the raw `String` and an expiration date.
public struct TestToken: Token {
public let raw: String
public let expiresAt: Date?
+
public init(raw: String, expiresAt: Date?) {
self.raw = raw
self.expiresAt = expiresAt
diff --git a/Sources/StructuredAPIClientTestSupport/TestTransport.swift b/Sources/StructuredAPIClientTestSupport/TestTransport.swift
index c756881..19cf0a0 100644
--- a/Sources/StructuredAPIClientTestSupport/TestTransport.swift
+++ b/Sources/StructuredAPIClientTestSupport/TestTransport.swift
@@ -16,9 +16,10 @@ import Foundation
@preconcurrency import FoundationNetworking
#endif
import StructuredAPIClient
+import AsyncHelpers
private final class TestTransportData: @unchecked Sendable {
- let lock = NSLock()
+ let lock = Locking.FastLock()
var history: [URLRequest]
var responses: [Result]
@@ -34,7 +35,7 @@ private final class TestTransportData: @unchecked Sendable {
}
}
-// A `Transport` that synchronously returns static values for tests
+/// A ``Transport`` that synchronously returns static values for tests
public final class TestTransport: Transport {
private let data: TestTransportData
let assertRequest: @Sendable (URLRequest) -> Void
diff --git a/Tests/StructuredAPIClientTests/NetworkClientTests.swift b/Tests/StructuredAPIClientTests/NetworkClientTests.swift
index deef1b8..3148447 100644
--- a/Tests/StructuredAPIClientTests/NetworkClientTests.swift
+++ b/Tests/StructuredAPIClientTests/NetworkClientTests.swift
@@ -17,9 +17,11 @@ import FoundationNetworking
#endif
@testable import StructuredAPIClient
import StructuredAPIClientTestSupport
+import AsyncHelpers
+import Logging
final class LockedResult: @unchecked Sendable {
- let lock = NSLock()
+ let lock = Locking.FastLock()
var result: Result?
var value: Result? {
@@ -31,6 +33,10 @@ final class LockedResult: @unchecked Sendable {
final class NetworkClientTests: XCTestCase {
private static let baseTestURL = URL(string: "https://test.somewhere.com")!
+ override class func setUp() {
+ XCTAssert(isLoggingConfigured)
+ }
+
private func _runTest(
request: R, client: NetworkClient,
file: StaticString = #filePath, line: UInt = #line
@@ -183,3 +189,16 @@ final class NetworkClientTests: XCTestCase {
try self.runTest(request: TestRequest(extraHeaders: ["H1": "1-3"]), client: client, expecting: "Test")
}
}
+
+func env(_ name: String) -> String? {
+ ProcessInfo.processInfo.environment[name]
+}
+
+let isLoggingConfigured: Bool = {
+ LoggingSystem.bootstrap { label in
+ var handler = StreamLogHandler.standardOutput(label: label)
+ handler.logLevel = env("LOG_LEVEL").flatMap { Logger.Level(rawValue: $0) } ?? .trace
+ return handler
+ }
+ return true
+}()
diff --git a/Tests/StructuredAPIClientTests/NetworkClientWithAsyncAwaitTests.swift b/Tests/StructuredAPIClientTests/NetworkClientWithAsyncAwaitTests.swift
index 1fb01d1..4b621b3 100644
--- a/Tests/StructuredAPIClientTests/NetworkClientWithAsyncAwaitTests.swift
+++ b/Tests/StructuredAPIClientTests/NetworkClientWithAsyncAwaitTests.swift
@@ -18,9 +18,11 @@ import FoundationNetworking
@testable import StructuredAPIClient
import StructuredAPIClientTestSupport
-@available(macOS 12.0, iOS 15.0, watchOS 8.0, tvOS 15.0, *)
final class NetworkClientWithAsyncAwaitTests: XCTestCase {
-
+ override class func setUp() {
+ XCTAssert(isLoggingConfigured)
+ }
+
func testNetworkClientWithAsyncAwait() async throws {
struct TestRequest: NetworkRequest {
func makeRequest(baseURL: URL) throws -> URLRequest { URLRequest(url: baseURL) }