Skip to content

Commit 6ec589c

Browse files
authored
feat(session): Async/await support (#4)
1 parent ff4250d commit 6ec589c

File tree

12 files changed

+150
-98
lines changed

12 files changed

+150
-98
lines changed

README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
# SimpleHTTP
22

3+
![swift](https://img.shields.io/badge/Swift-5.5%2B-orange?logo=swift&logoColor=white)
4+
![platforms](https://img.shields.io/badge/Platforms-iOS%20%7C%20macOS-lightgrey)
5+
![tests](https://github.com/pjechris/SimpleHTTP/actions/workflows/test.yml/badge.svg)
6+
[![twitter](https://img.shields.io/badge/twitter-pjechris-1DA1F2?logo=twitter&logoColor=white)](https://twitter.com/pjechris)
7+
38
Simple declarative HTTP API framework
49

510
## Basic Usage

Sources/SimpleHTTP/Foundation/Publisher+Validate.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ extension Publisher where Output == URLSession.DataTaskPublisher.Output {
1313
public func validate(_ converter: DataErrorConverter? = nil) -> AnyPublisher<Output, Error> {
1414
tryMap { output in
1515
do {
16-
try (output.response as? HTTPURLResponse)?.validate()
16+
try output.response.validate()
1717
return output
1818
}
1919
catch {

Sources/SimpleHTTP/Foundation/URLRequest/URLRequest+Encode.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import Foundation
44
import FoundationNetworking
55
#endif
66

7-
public extension URLRequest {
8-
func encodedBody(_ body: Encodable, encoder: ContentDataEncoder) throws -> Self {
7+
extension URLRequest {
8+
public func encodedBody(_ body: Encodable, encoder: ContentDataEncoder) throws -> Self {
99
var request = self
1010

1111
try request.encodeBody(body, encoder: encoder)
@@ -14,7 +14,7 @@ public extension URLRequest {
1414
}
1515

1616
/// Use a `Encodable` object as request body and set the "Content-Type" header associated to the encoder
17-
mutating func encodeBody(_ body: Encodable, encoder: ContentDataEncoder) throws {
17+
public mutating func encodeBody(_ body: Encodable, encoder: ContentDataEncoder) throws {
1818
httpBody = try body.encoded(with: encoder)
1919
setHeaders([.contentType: type(of: encoder).contentType.value])
2020
}

Sources/SimpleHTTP/Foundation/URLRequest/URLRequest+HTTPHeader.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import FoundationNetworking
55
#endif
66

77
extension URLRequest {
8+
/// Set the headers on the request
89
public mutating func setHeaders(_ headers: HTTPHeaderFields) {
910
for (header, value) in headers {
1011
setValue(value, forHTTPHeaderField: header.key)
1112
}
1213
}
1314

15+
/// Return a new `URLRequest`` with added `headers``
1416
public func settingHeaders(_ headers: HTTPHeaderFields) -> Self {
1517
var urlRequest = self
1618

Sources/SimpleHTTP/Foundation/URLRequest/URLRequest+URL.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Foundation
22

33
extension URLRequest {
44
/// Return a new URLRequest whose endpoint is relative to `baseURL`
5-
func relativeTo(_ baseURL: URL) -> URLRequest {
5+
public func relativeTo(_ baseURL: URL) -> URLRequest {
66
var urlRequest = self
77
var components = URLComponents(string: baseURL.appendingPathComponent(url?.path ?? "").absoluteString)
88

Sources/SimpleHTTP/Foundation/URLResponse+Validate.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,18 @@ import Foundation
44
import FoundationNetworking
55
#endif
66

7-
extension HTTPURLResponse {
8-
/// check whether a response is valid or not
7+
extension URLResponse {
8+
/// Validate when self is of type `HTTPURLResponse`
99
public func validate() throws {
10+
if let response = self as? HTTPURLResponse {
11+
try response.validateStatusCode()
12+
}
13+
}
14+
}
15+
16+
extension HTTPURLResponse {
17+
/// Throw an error when response status code is not Success (2xx)
18+
func validateStatusCode() throws {
1019
guard (200..<300).contains(statusCode) else {
1120
throw HTTPError(statusCode: statusCode)
1221
}

Sources/SimpleHTTP/Interceptor/Interceptor.swift

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,23 @@ public typealias Interceptor = RequestInterceptor & ResponseInterceptor
55

66
/// a protocol intercepting a session request
77
public protocol RequestInterceptor {
8-
/// Should be called before making the request to provide modifications to `request`
9-
func adaptRequest<Output>(_ request: Request<Output>) -> Request<Output>
10-
11-
/// catch and retry a failed request
12-
/// - Returns: nil if the request should not be retried. Otherwise a publisher that will be executed before
13-
/// retrying the request
14-
func rescueRequest<Output>(_ request: Request<Output>, error: Error) -> AnyPublisher<Void, Error>?
8+
/// Should be called before making the request to provide modifications to `request`
9+
func adaptRequest<Output>(_ request: Request<Output>) -> Request<Output>
10+
11+
/// catch and retry a failed request
12+
/// - Returns: nil if the request should not be retried. Otherwise a publisher that will be executed before
13+
/// retrying the request
14+
func rescueRequest<Output>(_ request: Request<Output>, error: Error) -> AnyPublisher<Void, Error>?
1515
}
1616

1717
/// a protocol intercepting a session response
1818
public protocol ResponseInterceptor {
19-
/// Should be called once the request is done and output was received. Let one last chance to modify the output
20-
/// optionally throwing an error instead if needed
21-
/// - Parameter request: the request that was sent to the server
22-
func adaptOutput<Output>(_ output: Output, for request: Request<Output>) throws -> Output
23-
24-
/// Notify of received response for `request`
25-
/// - Parameter request: the request that was sent to the server
26-
func receivedResponse<Output>(_ result: Result<Output, Error>, for request: Request<Output>)
19+
/// Should be called once the request is done and output was received. Let one last chance to modify the output
20+
/// optionally throwing an error instead if needed
21+
/// - Parameter request: the request that was sent to the server
22+
func adaptOutput<Output>(_ output: Output, for request: Request<Output>) throws -> Output
23+
24+
/// Notify of received response for `request`
25+
/// - Parameter request: the request that was sent to the server
26+
func receivedResponse<Output>(_ result: Result<Output, Error>, for request: Request<Output>)
2727
}

Sources/SimpleHTTP/Request/Request+URLRequest.swift

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,22 @@ import FoundationNetworking
55
#endif
66

77
extension Request {
8-
func toURLRequest(encoder: ContentDataEncoder) throws -> URLRequest {
8+
/// Transform a Request into a URLRequest
9+
/// - Parameter encoder: the encoder to use to encode the body is present
10+
/// - Parameter relativeTo: the base URL to append to the request path
11+
/// - Parameter accepting: if not nil will be used to set "Accept" header value
12+
public func toURLRequest(encoder: ContentDataEncoder, relativeTo baseURL: URL, accepting: ContentDataDecoder? = nil) throws -> URLRequest {
13+
let request = try toURLRequest(encoder: encoder)
14+
.relativeTo(baseURL)
15+
16+
if let decoder = accepting {
17+
return request.settingHeaders([.accept: type(of: decoder).contentType.value])
18+
}
19+
20+
return request
21+
}
22+
23+
private func toURLRequest(encoder: ContentDataEncoder) throws -> URLRequest {
924
var urlRequest = try URLRequest(url: URL(from: self))
1025

1126
urlRequest.httpMethod = method.rawValue.uppercased()
@@ -17,5 +32,7 @@ extension Request {
1732

1833
return urlRequest
1934
}
35+
36+
2037
}
2138

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import Foundation
2+
import Combine
3+
4+
#if canImport(_Concurrency)
5+
6+
extension Session {
7+
public func response<Output: Decodable>(for request: Request<Output>) async throws -> Output {
8+
try await response(publisher: publisher(for: request))
9+
}
10+
11+
public func response(for request: Request<Void>) async throws {
12+
try await response(publisher: publisher(for: request))
13+
}
14+
15+
private func response<Output>(publisher: AnyPublisher<Output, Error>) async throws -> Output {
16+
var cancellable: Set<AnyCancellable> = []
17+
let onCancel = { cancellable.removeAll() }
18+
19+
return try await withTaskCancellationHandler(
20+
handler: { onCancel() },
21+
operation: {
22+
try await withCheckedThrowingContinuation { continuation in
23+
publisher
24+
.sink(
25+
receiveCompletion: {
26+
if case let .failure(error) = $0 {
27+
return continuation.resume(throwing: error)
28+
}
29+
},
30+
receiveValue: {
31+
continuation.resume(returning: $0)
32+
})
33+
.store(in: &cancellable)
34+
}
35+
})
36+
}
37+
}
38+
39+
#endif

Sources/SimpleHTTP/Session/Session.swift

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,7 @@ extension Session {
7878

7979
do {
8080
let urlRequest = try adaptedRequest
81-
.toURLRequest(encoder: config.encoder)
82-
.relativeTo(baseURL)
83-
.settingHeaders([.accept: type(of: config.decoder).contentType.value])
81+
.toURLRequest(encoder: config.encoder, relativeTo: baseURL, accepting: config.decoder)
8482

8583
return urlRequestPublisher(urlRequest)
8684
.validate(config.errorConverter)

0 commit comments

Comments
 (0)