Skip to content

Commit 56678c5

Browse files
committed
Update server to take a courier that is used for the sending requests.
This separation allows us to more easily test the sending functionality, as well as inject a mock courier.
1 parent 6f84ed8 commit 56678c5

File tree

3 files changed

+78
-51
lines changed

3 files changed

+78
-51
lines changed
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
//
2+
// DefaultCourier.swift
3+
// ComposableArchitecturePattern
4+
//
5+
// Created by Jonathan Holland on 12/8/24.
6+
//
7+
8+
import Foundation
9+
import os
10+
11+
/// An actor that can be used for couriering requests.
12+
public actor DefaultCourier: Courier {
13+
/// A shared instance that can be used for couriering requests.
14+
public static let shared = DefaultCourier()
15+
16+
/// The `URLSession` to use for all server calls.
17+
public var urlSession: URLSession
18+
19+
/// The logger to use with communicating server courier activity.
20+
lazy var logger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.CAP.DefaultCourier", category: String(describing: Self.self))
21+
22+
/// Whether or not to log all activity wtih this server.
23+
var logActivity: LogActivity
24+
25+
/// Designated initializer.
26+
/// - Parameters:
27+
/// - urlSession: The session to use for making external calls.
28+
/// - logActivity: The log activity to use for processing requests.
29+
public init(urlSession: URLSession = .shared, logActivity: LogActivity = .all) {
30+
self.urlSession = urlSession
31+
self.logActivity = logActivity
32+
}
33+
34+
public func sendRequest(_ request: URLRequest, requestUID: UUID) async throws -> Data? {
35+
if self.logActivity == .all {
36+
self.logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) [Start]")
37+
}
38+
39+
let (data, response) = try await self.urlSession.data(for: request)
40+
41+
if self.logActivity == .all {
42+
self.logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) [Finish]")
43+
}
44+
45+
guard try response.analyzeAsHTTPResponse() else {
46+
self.logger.error("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) { Failed }")
47+
throw ServerAPIError.unknown(description: "Unable to complete server response.")
48+
}
49+
50+
if self.logActivity == .all {
51+
self.logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) { Success }")
52+
}
53+
54+
return data
55+
}
56+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//
2+
// Server+Courier.swift
3+
// ComposableArchitecturePattern
4+
//
5+
// Created by Jonathan Holland on 12/8/24.
6+
//
7+
8+
import Foundation
9+
10+
public protocol Courier {
11+
/// Send the given request to the server.
12+
/// - Returns: A boolean indicating the success of the request.
13+
/// - Throws: A `ServerAPIError` if unable to decode or an error encountered during the request.
14+
func sendRequest(_ request: URLRequest, requestUID: UUID) async throws -> Data?
15+
}

Sources/ComposableArchitecturePattern/Server.swift

Lines changed: 7 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ public protocol Server: Actor {
4040
/// The logger to use with communicating server activity.
4141
var logger: Logger { get }
4242

43-
/// The `URLSession` to use for all server calls.
44-
var urlSession: URLSession { get }
43+
/// The courier for making URL requests.
44+
///
45+
/// By default it will use a shared instance of `DefaultCourier`.
46+
var courier: Courier { get }
4547

4648
/// Sends a GET request and returns the specified value type from the given API.
4749
///
@@ -84,16 +86,6 @@ public protocol Server: Actor {
8486
/// - Note: `additionalHeaders` will override a key-value in `additionalHTTPHeaders`.
8587
/// - Note: The server automatically checks against these values to check whether they're supported by the API or not. For instance, if the return type of `Bool` is not supported, a `ServerAPIError.badRequest` error is thrown. If the specified API doesn't support this function, a `ServerAPIError.badRequest` error is thrown.
8688
func delete(using api: any ServerAPI, to endpoint: String?, additionalHeaders: [String: String]?, queries: [URLQueryItem]?, httpBodyOverride httpBody: Data?, timeoutInterval: TimeInterval?, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy?, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy?) async throws -> Bool
87-
88-
/// Send the given request to the server and return the decoded object.
89-
/// - Returns: The given decoded type or an `APIError`.
90-
/// - Throws: A `ServerAPIError` if unable to decode or an error encountered during the request.
91-
func sendRequest<T: Decodable>(_ request: URLRequest, requestUID: UUID, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy?, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy?) async throws -> T
92-
93-
/// Send the given request to the server.
94-
/// - Returns: A boolean indicating the success of the request.
95-
/// - Throws: A `ServerAPIError` if unable to decode or an error encountered during the request.
96-
func sendRequest(_ request: URLRequest, requestUID: UUID) async throws -> Bool
9789
}
9890

9991
public extension Server {
@@ -105,9 +97,7 @@ public extension Server {
10597
return Logger(subsystem: Bundle.main.bundleIdentifier ?? "com.Server", category: String(describing: Self.self))
10698
}
10799

108-
var urlSession: URLSession {
109-
URLSession.shared
110-
}
100+
var courier: Courier { DefaultCourier.shared }
111101

112102
var currentEnvironment: ServerEnvironment? { nil }
113103

@@ -341,27 +331,10 @@ public extension Server {
341331
}
342332

343333
func sendRequest<T: Decodable>(_ request: URLRequest, requestUID: UUID, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy? = nil, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy? = nil) async throws -> T {
344-
if self.logActivity == .all {
345-
self.logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) [Start]")
346-
}
347-
348-
let (data, response) = try await self.urlSession.data(for: request)
334+
let data = try await self.courier.sendRequest(request, requestUID: requestUID)
349335

350336
self.requestsBeingProcessed.remove(requestUID)
351337

352-
if self.logActivity == .all {
353-
self.logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) [Finish]")
354-
}
355-
356-
guard try response.analyzeAsHTTPResponse() else {
357-
self.logger.error("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) { Failed }")
358-
throw ServerAPIError.unknown(description: "Unable to complete server response.")
359-
}
360-
361-
if self.logActivity == .all {
362-
self.logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) { Success }")
363-
}
364-
365338
guard let data: T = try self._decode(data: data, dateDecodingStrategy: dateDecodingStrategy, keyDecodingStrategy: keyDecodingStrategy) else {
366339
throw ServerAPIError.unableToDecode(description: NSLocalizedString("Unable to decode object", comment: ""), error: nil)
367340
}
@@ -371,27 +344,10 @@ public extension Server {
371344
/// Send the given request to the server and return the result.
372345
/// - Returns: A result with `Void` or an `APIError`.
373346
func sendRequest(_ request: URLRequest, requestUID: UUID) async throws -> Bool {
374-
if self.logActivity == .all {
375-
logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) [Start]")
376-
}
377-
378-
let (_, response) = try await self.urlSession.data(for: request)
347+
let _ = try await self.courier.sendRequest(request, requestUID: requestUID)
379348

380349
self.requestsBeingProcessed.remove(requestUID)
381350

382-
if self.logActivity == .all {
383-
logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) [Finish]")
384-
}
385-
386-
guard try response.analyzeAsHTTPResponse() else {
387-
logger.error("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) { Failed }")
388-
throw ServerAPIError.unknown(description: "Unable to complete server response.")
389-
}
390-
391-
if self.logActivity == .all {
392-
logger.info("\(Date()) - (\(requestUID)) Request to \(String(describing: request.url?.description)) { Success }")
393-
}
394-
395351
return true
396352
}
397353

0 commit comments

Comments
 (0)