Skip to content

Commit 0239b23

Browse files
committed
Code review improvements
1 parent 9471682 commit 0239b23

6 files changed

Lines changed: 83 additions & 6 deletions

File tree

Sources/FTAPIKit/Endpoint.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ import Foundation
1010
/// cases and information about one endpoint is spreaded all over the files. Also,
1111
/// structs offer us generated initializers, which is very helpful.
1212
///
13+
/// - Important: Each endpoint should conform to at most one body-providing protocol
14+
/// (``DataEndpoint``, ``URLEncodedEndpoint``, ``MultipartEndpoint``, ``RequestEndpoint``,
15+
/// ``UploadEndpoint``). Conforming to multiple body protocols results in undefined body selection.
16+
///
1317
public protocol Endpoint {
1418

1519
/// URL path component without base URI.
@@ -35,34 +39,54 @@ public extension Endpoint {
3539
}
3640

3741
/// ``DataEndpoint`` transmits data provided in the ``FTAPIKit/DataEndpoint/body`` property without any further encoding.
42+
/// - Note: Default HTTP method is ``FTAPIKit/HTTPMethod/post``.
3843
public protocol DataEndpoint: Endpoint {
3944
var body: Data { get }
4045
}
4146

47+
public extension DataEndpoint {
48+
var method: HTTPMethod { .post }
49+
}
50+
4251
/// ``UploadEndpoint`` will send the provided file to the API.
4352
///
4453
/// - Note: If the standard implementation is used, `URLSession.uploadTask` methods will be used.
54+
/// Default HTTP method is ``FTAPIKit/HTTPMethod/post``.
4555
public protocol UploadEndpoint: Endpoint {
4656

4757
/// File which will be sent.
4858
var file: URL { get }
4959
}
5060

61+
public extension UploadEndpoint {
62+
var method: HTTPMethod { .post }
63+
}
64+
5165
/// Endpoint which will be sent as a multipart HTTP request.
5266
///
5367
/// - Note: If the standard implementation is used, the body parts will be merged into a temporary file, which will
5468
/// then be transformed to an input stream and passed to the request as a `httpBodyStream`.
69+
/// Default HTTP method is ``FTAPIKit/HTTPMethod/post``.
5570
public protocol MultipartEndpoint: Endpoint {
5671

5772
/// List of individual body parts.
5873
var parts: [MultipartBodyPart] { get }
5974
}
6075

76+
public extension MultipartEndpoint {
77+
var method: HTTPMethod { .post }
78+
}
79+
6180
/// The body of the endpoint with the URL query format.
81+
/// - Note: Default HTTP method is ``FTAPIKit/HTTPMethod/post``.
6282
public protocol URLEncodedEndpoint: Endpoint {
6383
var body: URLQuery { get }
6484
}
6585

86+
public extension URLEncodedEndpoint {
87+
var method: HTTPMethod { .post }
88+
}
89+
6690
/// An abstract representation of endpoint, body of which is represented by Swift encodable type. It serves as an
6791
/// abstraction between the ``URLServer`` protocol and more specific ``Endpoint`` conforming protocols.
6892
/// Do not use this protocol to represent an encodable endpoint, use ``RequestEndpoint`` instead.
@@ -90,7 +114,7 @@ public protocol ResponseEndpoint: Endpoint {
90114
public protocol RequestEndpoint: EncodableEndpoint {
91115
/// Associated type describing the encodable request model for serialization. The associated type is derived
92116
/// from the body property.
93-
associatedtype Request: Encodable
117+
associatedtype Request: Encodable & Sendable
94118
/// Generic encodable model, which will be sent in the body of the request.
95119
var request: Request { get }
96120
}

Sources/FTAPIKit/URLServer+Async.swift

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ public extension URLServer {
77
/// - endpoint: The endpoint
88
/// - configuring: Optional request configuration to apply before sending
99
/// - Throws: Throws an ``APIError`` if the request fails or server returns an error,
10-
/// or an error from ``RequestConfiguring/configure(_:)`` if configuration fails.
10+
/// `EncodingError` if request building fails, or an error from ``RequestConfiguring/configure(_:)``
11+
/// if configuration fails.
1112
func call(endpoint: Endpoint, configuring: RequestConfiguring? = nil) async throws {
1213
_ = try await execute(endpoint: endpoint, configuring: configuring)
1314
}
@@ -17,7 +18,8 @@ public extension URLServer {
1718
/// - endpoint: The endpoint
1819
/// - configuring: Optional request configuration to apply before sending
1920
/// - Throws: Throws an ``APIError`` if the request fails or server returns an error,
20-
/// or an error from ``RequestConfiguring/configure(_:)`` if configuration fails.
21+
/// `EncodingError` if request building fails, or an error from ``RequestConfiguring/configure(_:)``
22+
/// if configuration fails.
2123
/// - Returns: Plain data returned with the HTTP Response
2224
func call(data endpoint: Endpoint, configuring: RequestConfiguring? = nil) async throws -> Data {
2325
try await execute(endpoint: endpoint, configuring: configuring).data
@@ -28,7 +30,8 @@ public extension URLServer {
2830
/// - endpoint: The endpoint
2931
/// - configuring: Optional request configuration to apply before sending
3032
/// - Throws: Throws an ``APIError`` if the request fails or server returns an error,
31-
/// or an error from ``RequestConfiguring/configure(_:)`` if configuration fails.
33+
/// `EncodingError` if request building fails, or an error from ``RequestConfiguring/configure(_:)``
34+
/// if configuration fails.
3235
/// - Returns: Instance of the required type
3336
func call<EP: ResponseEndpoint>(response endpoint: EP, configuring: RequestConfiguring? = nil) async throws -> EP.Response {
3437
let result = try await execute(endpoint: endpoint, configuring: configuring)
@@ -46,10 +49,11 @@ public extension URLServer {
4649
/// - endpoint: The endpoint
4750
/// - configuring: Optional request configuration to apply before sending
4851
/// - Throws: Throws an ``APIError`` if the request fails or server returns an error,
49-
/// or an error from ``RequestConfiguring/configure(_:)`` if configuration fails.
52+
/// `EncodingError` if request building fails, or an error from ``RequestConfiguring/configure(_:)``
53+
/// if configuration fails.
5054
/// - Returns: The location of a temporary file where the server's response is stored.
5155
/// You must move this file or open it for reading before the async function returns. Otherwise, the file
52-
/// is deleted, and the data is lost.
56+
/// is deleted, and the data is lost. For automatic file management, use ``download(endpoint:destination:configuring:)`` instead.
5357
func download(endpoint: Endpoint, configuring: RequestConfiguring? = nil) async throws -> URL {
5458
let (urlRequest, observers) = try await prepareObservers(endpoint: endpoint, configuring: configuring)
5559

@@ -67,6 +71,19 @@ public extension URLServer {
6771

6872
return localURL
6973
}
74+
75+
/// Downloads a file from the specified endpoint and moves it to the given destination.
76+
/// - Parameters:
77+
/// - endpoint: The endpoint
78+
/// - destination: The file URL where the downloaded file should be stored
79+
/// - configuring: Optional request configuration to apply before sending
80+
/// - Throws: Throws an ``APIError`` if the request fails or server returns an error,
81+
/// `EncodingError` if request building fails, or an error from ``RequestConfiguring/configure(_:)``
82+
/// if configuration fails.
83+
func download(endpoint: Endpoint, destination: URL, configuring: RequestConfiguring? = nil) async throws {
84+
let tempURL = try await download(endpoint: endpoint, configuring: configuring)
85+
try FileManager.default.moveItem(at: tempURL, to: destination)
86+
}
7087
}
7188

7289
// MARK: - Private helpers

Templates/Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ INSTALL_DIR = ~/Library/Developer/Xcode/Templates/File\ Templates/FTAPIKit
33
install:
44
mkdir -p $(INSTALL_DIR)
55
cp -R Endpoints.xctemplate $(INSTALL_DIR)/
6+
@echo "Templates installed to $(INSTALL_DIR)"
67

78
uninstall:
89
rm -rf $(INSTALL_DIR)

Tests/FTAPIKitTests/EndpointTypeTests.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,14 @@ struct EndpointTypeTests {
5959
let url = try await server.download(endpoint: endpoint)
6060
#expect(FileManager.default.fileExists(atPath: url.path))
6161
}
62+
63+
@Test
64+
func downloadEndpointToDestination() async throws {
65+
let server = HTTPBinServer()
66+
let endpoint = ImageEndpoint()
67+
let destination = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
68+
try await server.download(endpoint: endpoint, destination: destination)
69+
#expect(FileManager.default.fileExists(atPath: destination.path))
70+
try FileManager.default.removeItem(at: destination)
71+
}
6272
}

Tests/FTAPIKitTests/Mockups/Endpoints.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,3 +97,10 @@ struct TestUploadEndpoint: UploadEndpoint {
9797
struct ImageEndpoint: Endpoint {
9898
let path = "image/jpeg"
9999
}
100+
101+
/// Endpoint that will succeed at the network level but fail during decoding,
102+
/// because the response from `/get` does not match `[Int]`.
103+
struct DecodingFailureEndpoint: ResponseEndpoint {
104+
typealias Response = [Int]
105+
let path = "get"
106+
}

Tests/FTAPIKitTests/NetworkObserverTests.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,24 @@ struct NetworkObserverTests {
6060
#expect(mockObserver.didFailCount == 1, "didFail should be called on failure")
6161
}
6262

63+
@Test
64+
func observerReceivesResponseBeforeDecodingFailure() async {
65+
let mockObserver = MockNetworkObserver()
66+
let server = HTTPBinServerWithObservers(observers: [mockObserver])
67+
let endpoint = DecodingFailureEndpoint()
68+
69+
do {
70+
_ = try await server.call(response: endpoint)
71+
Issue.record("Expected decoding error")
72+
} catch {
73+
// Expected decoding error
74+
}
75+
76+
#expect(mockObserver.willSendCount == 1, "willSendRequest should be called once")
77+
#expect(mockObserver.didReceiveCount == 1, "didReceiveResponse should be called even when decoding fails")
78+
#expect(mockObserver.didFailCount == 1, "didFail should be called for decoding error")
79+
}
80+
6381
@Test
6482
func multipleObserversAllReceiveCallbacks() async throws {
6583
let observer1 = MockNetworkObserver()

0 commit comments

Comments
 (0)