Skip to content

Commit 254359d

Browse files
committed
CR updates
1 parent 4d4bd72 commit 254359d

15 files changed

Lines changed: 324 additions & 70 deletions

.gitignore

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,11 +34,4 @@ FTAPIKit.xcodeproj
3434
# Claude Code
3535
.claude
3636

37-
# fastlane
38-
fastlane/report.xml
39-
fastlane/Preview.html
40-
fastlane/screenshots/
41-
fastlane/test_output/
42-
fastlane/README.md
43-
4437
*.xcuserstate

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ The framework is built around two core protocols:
8686
### Module Organization
8787

8888
**Source Structure** (`Sources/FTAPIKit/`):
89-
- Core protocols: `Server.swift`, `Endpoint.swift`
89+
- Core protocols: `URLServer.swift`, `Endpoint.swift`
9090
- Request building: `URLRequestBuilder.swift`, `RequestConfiguring.swift`
9191
- Async execution: `URLServer+Async.swift`, `URLServer+Download.swift`
9292
- Observers: `NetworkObserver.swift`

README.md

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<img align="right" alt="FTAPIKit logo" src="Sources/FTAPIKit/Documentation.docc/Resources/FTAPIKit.svg">
1+
<img align="right" alt="FTAPIKit logo" src="Sources/FTAPIKit/Documentation.docc/Resources/FTAPIKit.svg" height="65">
22

33
# FTAPIKit
44

@@ -45,10 +45,11 @@ are separated in various protocols for convenience.
4545

4646
- `Endpoint` protocol has empty body. Typically used in `GET` endpoints.
4747
- `DataEndpoint` sends provided data in body.
48-
- `UploadEndpoint` uploads file using `InputStream`.
48+
- `UploadEndpoint` uploads file from a URL using `URLSession` upload task.
4949
- `MultipartEndpoint` combines body parts into `InputStream` and sends them to server.
5050
Body parts are represented by `MultipartBodyPart` struct and provided to the endpoint
5151
in an array.
52+
- `URLEncodedEndpoint` sends body in URL query format.
5253
- `RequestEndpoint` has encodable request which is encoded using encoding
5354
of the `URLServer` instance.
5455

@@ -127,14 +128,7 @@ When we have server and endpoint defined we can call the web service using async
127128
let server = HTTPBinServer()
128129
let endpoint = UpdateUserEndpoint(request: user)
129130

130-
Task {
131-
do {
132-
let updatedUser = try await server.call(response: endpoint)
133-
// Handle success
134-
} catch {
135-
// Handle error
136-
}
137-
}
131+
let updatedUser = try await server.call(response: endpoint)
138132
```
139133

140134
### Async buildRequest
@@ -220,6 +214,38 @@ struct MyServer: URLServer {
220214
}
221215
```
222216

217+
### Error Handling
218+
219+
The framework uses the `APIError` protocol for error handling. The default implementation `APIError.Standard` covers common cases:
220+
221+
```swift
222+
do {
223+
let response = try await server.call(response: endpoint)
224+
} catch let error as APIError.Standard {
225+
switch error {
226+
case .connection(let urlError):
227+
// Network connectivity issue
228+
case .client(let statusCode, _, _):
229+
// 4xx client error
230+
case .server(let statusCode, _, _):
231+
// 5xx server error
232+
case .decoding(let decodingError):
233+
// Response parsing failed
234+
default:
235+
break
236+
}
237+
}
238+
```
239+
240+
For custom error parsing, define a type conforming to `APIError` and set it as the `ErrorType` on your server:
241+
242+
```swift
243+
struct MyServer: URLServer {
244+
typealias ErrorType = MyCustomError
245+
let baseUri = URL(string: "https://api.example.com")!
246+
}
247+
```
248+
223249
## Contributors
224250

225251
Current maintainer and main contributor is [Matěj Kašpar Jirásek](https://github.com/mkj-is), <matej.jirasek@futured.app>.

Sources/FTAPIKit/Coding.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ public protocol Encoding: Sendable {
88
func encode<T: Encodable>(_ object: T) throws -> Data
99

1010
/// Allows modification of `URLRequest`. Enables things like adding `Content-Type` header etc.
11+
///
12+
/// Default implementation is a no-op. Custom `Encoding` types should override this to set
13+
/// the appropriate `Content-Type` header. See ``JSONEncoding`` for an example.
1114
/// - Parameter request: Request which can be modified.
1215
func configure(request: inout URLRequest) throws
1316
}
@@ -21,7 +24,7 @@ public protocol Decoding: Sendable {
2124
func decode<T: Decodable>(data: Data) throws -> T
2225
}
2326

24-
/// Type-erased JSON encoder for use with types conforming to ``Server`` protocol.
27+
/// Type-erased JSON encoder for use with types conforming to ``URLServer`` protocol.
2528
public struct JSONEncoding: Encoding {
2629
private let encoder: JSONEncoder
2730

@@ -48,7 +51,7 @@ public struct JSONEncoding: Encoding {
4851
}
4952
}
5053

51-
/// Type-erased JSON decoder for use with types conforming to ``Server`` protocol.
54+
/// Type-erased JSON decoder for use with types conforming to ``URLServer`` protocol.
5255
public struct JSONDecoding: Decoding {
5356
private let decoder: JSONDecoder
5457

Sources/FTAPIKit/Documentation.docc/Documentation.md

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,19 @@
11
# ``FTAPIKit``
22

3-
Declarative, generic and protocol-oriented REST API framework using `URLSession` and `Codable`
3+
Declarative async/await REST API framework using Swift Concurrency and Codable.
44

55
## Overview
66

7-
Declarative and generic REST API framework using `Codable`.
8-
With standard implementation using `URLSesssion` and JSON encoder/decoder.
9-
Easily extensible for your asynchronous framework or networking stack.
7+
Declarative async/await REST API framework using `Codable`.
8+
With standard implementation using `URLSession` and JSON encoder/decoder.
9+
Built for Swift 6.1+ with full concurrency safety.
1010

1111
![Tree with a API Client root element. Its branches are servers. Each server branch has some endpoint branches.](Architecture)
1212

1313
## Topics
1414

1515
### Server
1616

17-
- ``Server``
1817
- ``URLServer``
1918

2019
### Endpoint
@@ -29,6 +28,10 @@ Easily extensible for your asynchronous framework or networking stack.
2928
- ``RequestEndpoint``
3029
- ``RequestResponseEndpoint``
3130

31+
### Request Configuration
32+
33+
- ``RequestConfiguring``
34+
3235
### Endpoint configuration
3336

3437
- ``HTTPMethod``
@@ -41,7 +44,10 @@ Easily extensible for your asynchronous framework or networking stack.
4144
- ``JSONEncoding``
4245
- ``Decoding``
4346
- ``JSONDecoding``
44-
- ``URLRequestEncoding``
47+
48+
### Observers
49+
50+
- ``NetworkObserver``
4551

4652
### Error handling
4753

Sources/FTAPIKit/Endpoint.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import Foundation
44
/// data and parameters which are sent to it.
55
///
66
/// Recommended conformance of this protocol is implemented using `struct`. It is
7-
/// of course possible using `enum` or `class`. Endpoints are are not designed
7+
/// of course possible using `enum` or `class`. Endpoints are not designed
88
/// to be referenced and used instantly after creation, so no memory usage is required.
99
/// The case for not using enums is long-term sustainability. Enums tend to have many
1010
/// cases and information about one endpoint is spreaded all over the files. Also,
@@ -64,7 +64,7 @@ public protocol URLEncodedEndpoint: Endpoint {
6464
}
6565

6666
/// An abstract representation of endpoint, body of which is represented by Swift encodable type. It serves as an
67-
/// abstraction between the ``Server`` protocol and more specific ``Endpoint`` conforming protocols.
67+
/// abstraction between the ``URLServer`` protocol and more specific ``Endpoint`` conforming protocols.
6868
/// Do not use this protocol to represent an encodable endpoint, use ``RequestEndpoint`` instead.
6969
public protocol EncodableEndpoint: Endpoint {
7070

@@ -78,7 +78,7 @@ public protocol EncodableEndpoint: Endpoint {
7878
/// for automatic deserialization.
7979
public protocol ResponseEndpoint: Endpoint {
8080
/// Associated type describing the return type conforming to `Decodable`
81-
/// protocol. This is only a phantom-type used by `APIAdapter`
81+
/// protocol. This is only a phantom-type used by ``URLServer``
8282
/// for automatic decoding/deserialization of API results.
8383
associatedtype Response: Decodable & Sendable
8484
}

Sources/FTAPIKit/NetworkObserver.swift

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ import Foundation
88
/// The `Context` associated type allows passing correlation data (request ID, start time, etc.)
99
/// through the request lifecycle:
1010
/// 1. `willSendRequest` is called before the request starts and returns a `Context` value
11-
/// 2. `didReceiveResponse` is always called with the raw response data (useful for debugging)
11+
/// 2. `didReceiveResponse` is called when a response is received from the server
1212
/// 3. `didFail` is called additionally if the request processing fails (network, HTTP status, or decoding error)
13-
/// 4. If the observer is deallocated before the request completes, the context is discarded
14-
/// and no completion callback is invoked
13+
///
14+
/// - Note: Observers are strongly retained for the duration of a request to ensure lifecycle callbacks
15+
/// are always delivered.
1516
public protocol NetworkObserver: AnyObject, Sendable {
1617
associatedtype Context: Sendable
1718

@@ -22,8 +23,8 @@ public protocol NetworkObserver: AnyObject, Sendable {
2223

2324
/// Called when a response is received from the server.
2425
///
25-
/// This is always called with the raw response data, even if processing subsequently fails.
26-
/// This allows observers to inspect the actual response for debugging purposes.
26+
/// Only called when the server responds. Not called on network errors (e.g. no connectivity).
27+
/// Called even if the response indicates an error (4xx, 5xx), before `didFail`.
2728
/// - Parameters:
2829
/// - request: The original request
2930
/// - response: The URL response (may be HTTPURLResponse)

Sources/FTAPIKit/URLServer+Async.swift

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,7 @@ private extension URLServer {
5151
/// Core execution method that builds the request, notifies observers, performs the network call,
5252
/// and handles errors.
5353
func execute(endpoint: Endpoint, configuring: RequestConfiguring?) async throws -> ExecuteResult {
54-
var urlRequest = try await buildRequest(endpoint: endpoint)
55-
try await configuring?.configure(&urlRequest)
56-
57-
let observers = networkObservers.map { AnyObserverToken(observer: $0, request: urlRequest) }
54+
let (urlRequest, observers) = try await prepareRequest(endpoint: endpoint, configuring: configuring)
5855

5956
let file = (endpoint as? UploadEndpoint)?.file
6057

@@ -66,19 +63,43 @@ private extension URLServer {
6663
(data, response) = try await urlSession.data(for: urlRequest)
6764
}
6865
} catch {
69-
observers.forEach { $0.didReceiveResponse(for: urlRequest, response: nil, data: nil) }
7066
observers.forEach { $0.didFail(request: urlRequest, error: error) }
7167
throw error
7268
}
7369

7470
observers.forEach { $0.didReceiveResponse(for: urlRequest, response: response, data: data) }
71+
try checkForError(data: data, response: response, request: urlRequest, observers: observers)
72+
73+
return ExecuteResult(data: data, request: urlRequest, observers: observers)
74+
}
75+
}
76+
77+
// MARK: - Shared helpers
7578

79+
extension URLServer {
80+
81+
/// Builds the URLRequest for the endpoint, applies optional configuration, and creates observer tokens.
82+
func prepareRequest(
83+
endpoint: Endpoint,
84+
configuring: RequestConfiguring?
85+
) async throws -> (URLRequest, [AnyObserverToken]) {
86+
var urlRequest = try await buildRequest(endpoint: endpoint)
87+
try await configuring?.configure(&urlRequest)
88+
let observers = networkObservers.map { AnyObserverToken(observer: $0, request: urlRequest) }
89+
return (urlRequest, observers)
90+
}
91+
92+
/// Checks the response for API errors and notifies observers on failure.
93+
func checkForError(
94+
data: Data?,
95+
response: URLResponse,
96+
request: URLRequest,
97+
observers: [AnyObserverToken]
98+
) throws {
7699
if let error = ErrorType(data: data, response: response, error: nil, decoding: decoding) {
77-
observers.forEach { $0.didFail(request: urlRequest, error: error) }
100+
observers.forEach { $0.didFail(request: request, error: error) }
78101
throw error
79102
}
80-
81-
return ExecuteResult(data: data, request: urlRequest, observers: observers)
82103
}
83104
}
84105

Sources/FTAPIKit/URLServer+Download.swift

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,18 @@ public extension URLServer {
1111
/// You must move this file or open it for reading before the async function returns. Otherwise, the file
1212
/// is deleted, and the data is lost.
1313
func download(endpoint: Endpoint, configuring: RequestConfiguring? = nil) async throws -> URL {
14-
var urlRequest = try await buildRequest(endpoint: endpoint)
15-
try await configuring?.configure(&urlRequest)
16-
17-
let observers = networkObservers.map { AnyObserverToken(observer: $0, request: urlRequest) }
14+
let (urlRequest, observers) = try await prepareRequest(endpoint: endpoint, configuring: configuring)
1815

1916
let (localURL, response): (URL, URLResponse)
2017
do {
2118
(localURL, response) = try await urlSession.download(for: urlRequest)
2219
} catch {
23-
observers.forEach { $0.didReceiveResponse(for: urlRequest, response: nil, data: nil) }
2420
observers.forEach { $0.didFail(request: urlRequest, error: error) }
2521
throw error
2622
}
2723

2824
observers.forEach { $0.didReceiveResponse(for: urlRequest, response: response, data: nil) }
29-
30-
let urlData = localURL.absoluteString.data(using: .utf8)
31-
if let error = ErrorType(data: urlData, response: response, error: nil, decoding: decoding) {
32-
observers.forEach { $0.didFail(request: urlRequest, error: error) }
33-
throw error
34-
}
25+
try checkForError(data: nil, response: response, request: urlRequest, observers: observers)
3526

3627
return localURL
3728
}

Sources/FTAPIKit/URLServer.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import Foundation
1111
/// In case that the requests need to cooperate with other services, like OAuth, override the
1212
/// default implementation of `buildRequest`, use `buildStandardRequest(endpoint:)` within
1313
/// your new implementation, and use the `URLRequest` as a baseline.
14-
public protocol URLServer {
14+
public protocol URLServer: Sendable {
1515
/// Error type which is initialized during the request execution.
1616
associatedtype ErrorType: APIError = APIError.Standard
1717

0 commit comments

Comments
 (0)