Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .swift-format
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"version": 1,
"lineLength": 120,
"lineLength": 100000,
"indentation": {
"spaces": 4
},
Expand Down
7 changes: 2 additions & 5 deletions Sources/Networking/CacheStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ final class CacheStore: @unchecked Sendable {
let folderPath = "\(folderName)/\(Self.shardName(for: component))"
let finalPath = "\(folderPath)/\(component)"

guard let url = URL(string: finalPath),
let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first
else {
guard let url = URL(string: finalPath), let cachesURL = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first else {
throw NSError(
domain: folderName,
code: 9999,
Expand Down Expand Up @@ -126,8 +124,7 @@ final class CacheStore: @unchecked Sendable {
memory.setObject(returnedObject as AnyObject, forKey: key as AnyObject)
// Re-warm: bump the file's mtime so an entry in active use never expires. Only happens
// on a memory miss, so it's ~once per entry per launch — no explicit debounce needed.
try? FileManager.default.setAttributes(
[.modificationDate: Date()], ofItemAtPath: destinationURL.path)
try? FileManager.default.setAttributes([.modificationDate: Date()], ofItemAtPath: destinationURL.path)
}

return returnedObject
Expand Down
4 changes: 1 addition & 3 deletions Sources/Networking/FakeRequest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,7 @@ struct FakeRequest {
let lookupPathParts = evaluatedPath.components(separatedBy: "/")

for (originalFakedPath, fakeRequest) in requests {
guard let replacedValues = captures(from: originalFakedPath, matching: lookupPathParts),
originalFakedPath.replacing(replacedValues) == path
else { continue }
guard let replacedValues = captures(from: originalFakedPath, matching: lookupPathParts), originalFakedPath.replacing(replacedValues) == path else { continue }

guard case .data(let data) = fakeRequest.payload, let responseString = String(data: data, encoding: .utf8)
else { continue }
Expand Down
27 changes: 6 additions & 21 deletions Sources/Networking/HTTPInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,7 @@ public struct HTTPExchange: Sendable {
/// A composable hook wrapping every verb request. `next` runs the rest of the chain (innermost is the real
/// network call); calling it again replays the request — the basis for retry and auth-refresh.
public protocol HTTPInterceptor: Sendable {
func intercept(
_ request: URLRequest,
next: @Sendable (URLRequest) async throws -> HTTPExchange
) async throws
func intercept(_ request: URLRequest, next: @Sendable (URLRequest) async throws -> HTTPExchange) async throws
-> HTTPExchange
}

Expand All @@ -40,10 +37,7 @@ public struct AuthRefreshInterceptor: HTTPInterceptor {
self.coordinator = RefreshCoordinator(refresh)
}

public func intercept(
_ request: URLRequest,
next: @Sendable (URLRequest) async throws -> HTTPExchange
) async throws -> HTTPExchange {
public func intercept(_ request: URLRequest, next: @Sendable (URLRequest) async throws -> HTTPExchange) async throws -> HTTPExchange {
let exchange = try await next(request)
guard triggeringStatusCodes.contains(exchange.response.statusCode) else { return exchange }
guard let refreshedValue = try await coordinator.refreshOnce() else { return exchange }
Expand Down Expand Up @@ -81,10 +75,7 @@ public struct RetryInterceptor: HTTPInterceptor {
self.retryableMethods = Set(retryableMethods.map { $0.uppercased() })
}

public func intercept(
_ request: URLRequest,
next: @Sendable (URLRequest) async throws -> HTTPExchange
) async throws -> HTTPExchange {
public func intercept(_ request: URLRequest, next: @Sendable (URLRequest) async throws -> HTTPExchange) async throws -> HTTPExchange {
let methodAllowsRetry = retryableMethods.contains((request.httpMethod ?? "GET").uppercased())
var attempt = 1
while true {
Expand Down Expand Up @@ -115,9 +106,7 @@ public struct RetryInterceptor: HTTPInterceptor {

// Retry-After is either a count of seconds or an HTTP-date (RFC 9110).
static func retryAfterDelay(from response: HTTPURLResponse) -> Duration? {
guard let value = response.value(forHTTPHeaderField: "Retry-After")?.trimmingCharacters(in: .whitespaces),
!value.isEmpty
else { return nil }
guard let value = response.value(forHTTPHeaderField: "Retry-After")?.trimmingCharacters(in: .whitespaces), !value.isEmpty else { return nil }
if let seconds = Int(value) {
return .seconds(max(0, seconds))
}
Expand Down Expand Up @@ -145,18 +134,14 @@ public struct ResponseValidatorInterceptor: HTTPInterceptor {
self.validate = validate
}

public func intercept(
_ request: URLRequest,
next: @Sendable (URLRequest) async throws -> HTTPExchange
) async throws -> HTTPExchange {
public func intercept(_ request: URLRequest, next: @Sendable (URLRequest) async throws -> HTTPExchange) async throws -> HTTPExchange {
let exchange = try await next(request)
guard (200..<300).contains(exchange.response.statusCode) else { return exchange }
switch validate(exchange) {
case .valid:
return exchange
case .invalid(let reason):
throw NetworkingError.validation(
reason: reason, ResponseMetadata(response: exchange.response, body: exchange.data))
throw NetworkingError.validation(reason: reason, ResponseMetadata(response: exchange.response, body: exchange.data))
}
}
}
Expand Down
4 changes: 1 addition & 3 deletions Sources/Networking/Helpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,7 @@ extension Dictionary where Key: ExpressibleByStringLiteral {
public func urlEncodedString() throws -> String {

let pairs = try reduce([]) { current, keyValuePair -> [String] in
if let encodedValue = "\(keyValuePair.value)".addingPercentEncoding(
withAllowedCharacters: .urlQueryParametersAllowed)
{
if let encodedValue = "\(keyValuePair.value)".addingPercentEncoding(withAllowedCharacters: .urlQueryParametersAllowed) {
return current + ["\(keyValuePair.key)=\(encodedValue)"]
} else {
throw NSError(
Expand Down
3 changes: 1 addition & 2 deletions Sources/Networking/JSONResponse.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,7 @@ public struct AnyCodable: Decodable, @unchecked Sendable {
} else if let value = try? container.decode([AnyCodable].self) {
self.value = value
} else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: "The container contains nothing serializable")
throw DecodingError.dataCorruptedError(in: container, debugDescription: "The container contains nothing serializable")
}
}

Expand Down
6 changes: 2 additions & 4 deletions Sources/Networking/Networking+New.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,7 @@ extension Networking {
)
{
// Run the cache hit back out through the interceptor chain so validators apply to it too.
let exchange = try await perform(
request, cached: HTTPExchange(data: cached.body, response: cachedResponse))
let exchange = try await perform(request, cached: HTTPExchange(data: cached.body, response: cachedResponse))
result = handleResponse(
responseData: exchange.data,
response: exchange.response,
Expand Down Expand Up @@ -490,8 +489,7 @@ extension Networking {
case .cancelled:
return .failure(.cancelled)
case .redirection, .clientError, .serverError, .unknown:
let error = HTTPError(
statusCode: statusCode, metadata: ResponseMetadata(response: httpResponse, body: responseData))
let error = HTTPError(statusCode: statusCode, metadata: ResponseMetadata(response: httpResponse, body: responseData))
return .failure(.http(error))
}
}
Expand Down
6 changes: 2 additions & 4 deletions Sources/Networking/Networking.swift
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,7 @@ public actor Networking {
/// A stream of observability events — one `.started` then one `.completed` per request. Each call
/// returns its own multicast stream; the buffer keeps the newest 256 events for a slow consumer.
public func events() -> AsyncStream<NetworkingEvent> {
let (stream, continuation) = AsyncStream.makeStream(
of: NetworkingEvent.self, bufferingPolicy: .bufferingNewest(256))
let (stream, continuation) = AsyncStream.makeStream(of: NetworkingEvent.self, bufferingPolicy: .bufferingNewest(256))
let id = UUID()
streamContinuations[id] = continuation
continuation.onTermination = { [weak self] _ in
Expand Down Expand Up @@ -208,8 +207,7 @@ public actor Networking {
folderName: Networking.domain
)
self.logger = logger ?? Networking.defaultLogger
self.logFileURL = ProcessInfo.processInfo.environment["NETWORKING_LOG_FILE"].flatMap(
Networking.resolveLogFileURL)
self.logFileURL = ProcessInfo.processInfo.environment["NETWORKING_LOG_FILE"].flatMap(Networking.resolveLogFileURL)
// Sweep aged-out files off the request path so the disk cache can't grow without bound (one shard
// per launch — see `CacheStore.sweepExpired`).
let store = self.cacheStore
Expand Down
6 changes: 2 additions & 4 deletions Tests/NetworkingTests/CacheStoreTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,7 @@ final class CacheStoreTests: XCTestCase {

let url = try store.destinationURL(forResource: resource)
memory.removeObject(forKey: url.absoluteString as AnyObject) // force the disk path
try FileManager.default.setAttributes(
[.modificationDate: Date(timeIntervalSinceNow: -120)], ofItemAtPath: url.path)
try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSinceNow: -120)], ofItemAtPath: url.path)

XCTAssertNil(
try store.object(
Expand All @@ -88,8 +87,7 @@ final class CacheStoreTests: XCTestCase {

let url = try store.destinationURL(forResource: resource)
memory.removeObject(forKey: url.absoluteString as AnyObject) // force the disk path
try FileManager.default.setAttributes(
[.modificationDate: Date(timeIntervalSinceNow: -30)], ofItemAtPath: url.path)
try FileManager.default.setAttributes([.modificationDate: Date(timeIntervalSinceNow: -30)], ofItemAtPath: url.path)

XCTAssertNotNil(
try store.object(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,7 @@ final class CacheWriteAndDataBodyRegressionTests: XCTestCase {
guard case .success = result else { return XCTFail("expected a successful download, got \(result)") }

let orphanURL = try networking.destinationURL(for: path, cacheName: nil)
XCTAssertFalse(
FileManager.default.exists(at: orphanURL),
"downloadData wrote a stray cache file under the path key that no read path uses")
XCTAssertFalse(FileManager.default.exists(at: orphanURL), "downloadData wrote a stray cache file under the path key that no read path uses")
}

// The `.memory`/`.none` pure-read contracts (a read must not destroy a durable `.memoryAndFile` copy)
Expand Down
3 changes: 1 addition & 2 deletions Tests/NetworkingTests/DELETEIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,7 @@ final class DELETEIntegrationTests: XCTestCase {

func testDELETEWithURLEncodedParameters() async throws {
let networking = Networking(baseURL: baseURL)
let result: Result<JSONResponse, NetworkingError> = await networking.delete(
"/delete", query: [URLQueryItem(name: "userId", value: "25")])
let result: Result<JSONResponse, NetworkingError> = await networking.delete("/delete", query: [URLQueryItem(name: "userId", value: "25")])
switch result {
case .success(let response):
XCTAssertEqual(response.body.string(for: "url"), "\(TestConfig.httpbinBaseURL)/delete?userId=25")
Expand Down
4 changes: 1 addition & 3 deletions Tests/NetworkingTests/DownloadEventsIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,7 @@ final class DownloadEventsIntegrationTests: XCTestCase {
2,
"downloadImage should emit .started and .completed; got \(events)"
)
guard case .started(let startContext) = events.first,
case .completed(let endContext, let outcome, _, _) = events.last
else {
guard case .started(let startContext) = events.first, case .completed(let endContext, let outcome, _, _) = events.last else {
return XCTFail("expected .started then .completed, got \(events)")
}
XCTAssertEqual(startContext.id, endContext.id)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,7 @@ final class DownloadInterceptorIntegrationTests: XCTestCase {
private struct CountingPassthroughInterceptor: HTTPInterceptor {
let counter: Counter

func intercept(
_ request: URLRequest,
next: @Sendable (URLRequest) async throws -> HTTPExchange
) async throws -> HTTPExchange {
func intercept(_ request: URLRequest, next: @Sendable (URLRequest) async throws -> HTTPExchange) async throws -> HTTPExchange {
await counter.tick()
return try await next(request)
}
Expand Down
12 changes: 4 additions & 8 deletions Tests/NetworkingTests/FakeRequestTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,7 @@ extension FakeRequestTests {

await networking.fakePOST("/story", response: [["name": "Elvis"]])

let result: Result<[[String: AnyCodable]], NetworkingError> = await networking.post(
"/story", body: ["username": "jameson", "password": "secret"])
let result: Result<[[String: AnyCodable]], NetworkingError> = await networking.post("/story", body: ["username": "jameson", "password": "secret"])
switch result {
case .success(let stories):
XCTAssertEqual(stories.first?.string(for: "name"), "Elvis")
Expand Down Expand Up @@ -536,8 +535,7 @@ extension FakeRequestTests {

await networking.fakePUT("/story", response: [["name": "Elvis"]])

let result: Result<[[String: AnyCodable]], NetworkingError> = await networking.put(
"/story", body: ["username": "jameson", "password": "secret"])
let result: Result<[[String: AnyCodable]], NetworkingError> = await networking.put("/story", body: ["username": "jameson", "password": "secret"])
switch result {
case .success(let stories):
XCTAssertEqual(stories.first?.string(for: "name"), "Elvis")
Expand Down Expand Up @@ -603,8 +601,7 @@ extension FakeRequestTests {

await networking.fakePATCH("/story", response: [["name": "Elvis"]])

let result: Result<[[String: AnyCodable]], NetworkingError> = await networking.patch(
"/story", body: ["username": "jameson", "password": "secret"])
let result: Result<[[String: AnyCodable]], NetworkingError> = await networking.patch("/story", body: ["username": "jameson", "password": "secret"])
switch result {
case .success(let stories):
XCTAssertEqual(stories.first?.string(for: "name"), "Elvis")
Expand Down Expand Up @@ -856,8 +853,7 @@ extension FakeRequestTests {
"confirmation_code": "confirmationCode"
]

let result: Result<JSONResponse, NetworkingError> = await networking.post(
"/auth/verify_confirmation_code", body: parameters)
let result: Result<JSONResponse, NetworkingError> = await networking.post("/auth/verify_confirmation_code", body: parameters)
switch result {
case .success(let response):
let headers = response.headers
Expand Down
11 changes: 3 additions & 8 deletions Tests/NetworkingTests/FileLoggingIntegrationTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ final class FileLoggingIntegrationTests: XCTestCase {
// The diagnostics file sink lets a CLI / headless / agent run read what happened, since os.Logger
// isn't visible in stdout. (The NETWORKING_LOG_FILE env var sets the same `logFileURL` at init.)
func testDiagnosticsAreWrittenToConfiguredFile() async throws {
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent(
"networking-\(UUID().uuidString).log")
let url = URL(fileURLWithPath: NSTemporaryDirectory()).appendingPathComponent("networking-\(UUID().uuidString).log")
defer { try? FileManager.default.removeItem(at: url) }

let networking = Networking(baseURL: TestConfig.httpbinBaseURL)
Expand All @@ -20,18 +19,14 @@ final class FileLoggingIntegrationTests: XCTestCase {
let contents = (try? String(contentsOf: url, encoding: .utf8)) ?? ""
XCTAssertFalse(contents.contains("/get "), "successful requests must not be logged; got:\n\(contents)")
XCTAssertTrue(contents.contains("✗ GET"), "expected a failure line; got:\n\(contents)")
XCTAssertTrue(
contents.contains("failed: The server returned status 404"),
"expected the failure detail; got:\n\(contents)")
XCTAssertTrue(contents.contains("failed: The server returned status 404"), "expected the failure detail; got:\n\(contents)")
}

// NETWORKING_LOG_FILE resolution: a bare name lands in Caches (sandbox-safe for an app); a path is used as-is.
func testLogFileEnvValueResolution() {
let bare = Networking.resolveLogFileURL("networking.log")
XCTAssertEqual(bare?.lastPathComponent, "networking.log")
XCTAssertTrue(
bare?.path.contains("Caches") ?? false,
"a bare name should resolve under Caches; got \(String(describing: bare?.path))")
XCTAssertTrue(bare?.path.contains("Caches") ?? false, "a bare name should resolve under Caches; got \(String(describing: bare?.path))")

XCTAssertEqual(Networking.resolveLogFileURL("/tmp/explicit.log")?.path, "/tmp/explicit.log")
XCTAssertNil(Networking.resolveLogFileURL(""))
Expand Down
Loading