Skip to content

Commit a758a08

Browse files
committed
Change to a protocol
1 parent f02de59 commit a758a08

File tree

5 files changed

+142
-41
lines changed

5 files changed

+142
-41
lines changed

.swiftlint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ disabled_rules:
22
- closure_parameter_position
33
- identifier_name
44
- multiple_closures_with_trailing_closure
5+
- type_name
56
opt_in_rules:
67
- attributes
78
- closure_end_indentation
@@ -11,7 +12,6 @@ opt_in_rules:
1112
- contains_over_filter_is_empty
1213
- contains_over_first_not_nil
1314
- contains_over_range_nil_comparison
14-
- convenience_type
1515
- empty_count
1616
- empty_string
1717
- explicit_init

Package.swift

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ let package = Package(
1111
.visionOS(.v1)
1212
],
1313
products: [
14-
.library(name: "EndpointBuilderURLSession", targets: ["EndpointBuilderURLSession"]),
14+
.library(name: "EndpointBuilderURLSession", targets: ["EndpointBuilderURLSession"])
1515
],
1616
dependencies: [
1717
.package(url: "https://github.com/apple/swift-http-types", from: "1.0.0"),
@@ -24,6 +24,12 @@ let package = Package(
2424
.product(name: "EndpointBuilder", package: "endpoint-builder"),
2525
.product(name: "HTTPTypes", package: "swift-http-types")
2626
]
27+
),
28+
.testTarget(
29+
name: "EndpointBuilderURLSessionTests",
30+
dependencies: [
31+
.byName(name: "EndpointBuilderURLSession")
32+
]
2733
)
2834
]
2935
)

Sources/EndpointBuilderURLSession/EndpointBuilderURLSession.swift

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,35 @@ import FoundationNetworking
66
import HTTPTypes
77

88
/// An API endpoint client that uses `URLSession` to create requests
9-
public struct EndpointBuilderURLSession: Sendable {
9+
public protocol EndpointBuilderURLSession: Sendable {
1010

1111
/// The base server URL on which endpoint paths will be appended
12-
public let serverBaseURL: URL
12+
var serverBaseURL: URL { get }
1313

1414
/// The URLSession object that will make requests
15-
public let urlSession: @Sendable () -> URLSession
15+
var urlSession: @Sendable () -> URLRequestHandler { get }
1616

1717
/// JSON encoder
18-
public let encoder: @Sendable () -> JSONEncoder
18+
var encoder: @Sendable () -> JSONEncoder { get }
1919

2020
/// JSON decoder
21-
public let decoder: @Sendable () -> JSONDecoder
22-
23-
/// Creates a new `EndpointBuilderURLSession`.
24-
public init(
25-
serverBaseURL: URL,
26-
urlSession: @Sendable @escaping () -> URLSession = { URLSession.shared },
27-
encoder: @Sendable @escaping () -> JSONEncoder = { JSONEncoder() },
28-
decoder: @Sendable @escaping () -> JSONDecoder = { JSONDecoder() }
29-
) {
30-
self.serverBaseURL = serverBaseURL
31-
self.urlSession = urlSession
32-
self.encoder = encoder
33-
self.decoder = decoder
21+
var decoder: @Sendable () -> JSONDecoder { get }
22+
23+
}
24+
25+
// Default values
26+
extension EndpointBuilderURLSession {
27+
28+
public var urlSession: @Sendable () -> URLSession {
29+
{ URLSession.shared }
30+
}
31+
32+
public var encoder: @Sendable () -> JSONEncoder {
33+
{ JSONEncoder() }
34+
}
35+
36+
var decoder: @Sendable () -> JSONDecoder {
37+
{ JSONDecoder() }
3438
}
3539

3640
}
@@ -70,7 +74,8 @@ extension EndpointBuilderURLSession {
7074
}
7175

7276
// perform request
73-
return try await urlSession().responseData(for: request)
77+
let (data, _) = try await urlSession().data(for: request)
78+
return data
7479
}
7580

7681
}
@@ -90,24 +95,3 @@ extension URL {
9095
}
9196

9297
}
93-
94-
extension URLSession {
95-
96-
func responseData(for request: URLRequest) async throws -> Data {
97-
#if canImport(FoundationNetworking)
98-
await withCheckedContinuation { continuation in
99-
self.dataTask(with: request) { data, _, _ in
100-
guard let data = data else {
101-
continuation.resume(returning: Data())
102-
return
103-
}
104-
continuation.resume(returning: data)
105-
}.resume()
106-
}
107-
#else
108-
let (data, _) = try await self.data(for: request)
109-
return data
110-
#endif
111-
}
112-
113-
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Foundation
2+
#if canImport(FoundationNetworking)
3+
import FoundationNetworking
4+
#endif
5+
6+
public protocol URLRequestHandler: Sendable {
7+
8+
func data(for request: URLRequest) async throws -> (Data, URLResponse)
9+
10+
}
11+
12+
extension URLSession: URLRequestHandler {}
13+
14+
#if canImport(FoundationNetworking)
15+
enum URLRequestHandlerError: Error {
16+
case noResponse
17+
}
18+
19+
extension URLSession {
20+
21+
public func data(for request: URLRequest) async throws -> (Data, URLResponse) {
22+
try await withCheckedThrowingContinuation { continuation in
23+
self.dataTask(with: request) { data, response, error in
24+
guard let data = data, let response = response else {
25+
continuation.resume(throwing: error ?? URLRequestHandlerError.noResponse)
26+
return
27+
}
28+
continuation.resume(returning: (data, response))
29+
}.resume()
30+
}
31+
}
32+
33+
}
34+
#endif
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import EndpointBuilder
2+
@testable import EndpointBuilderURLSession
3+
import HTTPTypes
4+
import RoutingKit
5+
import XCTest
6+
#if canImport(FoundationNetworking)
7+
import FoundationNetworking
8+
#endif
9+
10+
struct MockURLSession: URLRequestHandler {
11+
12+
let requestHandler: @Sendable (URLRequest) -> Data
13+
14+
func data(for request: URLRequest) async throws -> (Data, URLResponse) {
15+
let data = requestHandler(request)
16+
return (
17+
data,
18+
URLResponse()
19+
)
20+
}
21+
22+
}
23+
24+
final class EndpointBuilderURLSessionTests: XCTestCase {
25+
26+
@Endpoint
27+
struct EndpointWithNoResponse {
28+
static let path: [PathComponent] = ["blank"]
29+
static let httpMethod = HTTPRequest.Method.get
30+
}
31+
32+
@Endpoint
33+
struct EndpointWithStringResponseAndPathComponent {
34+
static let path: [PathComponent] = ["echo", ":id"]
35+
static let httpMethod = HTTPRequest.Method.post
36+
static let responseType = String.self
37+
let body: String
38+
}
39+
40+
struct APIClient: EndpointBuilderURLSession {
41+
42+
let serverBaseURL = URL(string: "https://api.shipyard.studio")!
43+
let urlSession: @Sendable () -> URLRequestHandler
44+
45+
init(mockSession: MockURLSession) {
46+
let session: @Sendable () -> URLRequestHandler = { mockSession }
47+
self.urlSession = session
48+
}
49+
50+
}
51+
52+
func testEndpointWithNoResponse() async throws {
53+
let endpoint = EndpointWithNoResponse()
54+
let client = APIClient(mockSession: MockURLSession(requestHandler: { urlRequest in
55+
XCTAssertEqual(urlRequest.url?.pathComponents, ["/", "blank"])
56+
XCTAssertEqual(urlRequest.httpMethod, "GET")
57+
return Data()
58+
}))
59+
try await client.request(endpoint)
60+
}
61+
62+
func testEndpointWithStringResponse() async throws {
63+
let endpoint = EndpointWithStringResponseAndPathComponent(
64+
body: "hello",
65+
pathParameters: EndpointWithStringResponseAndPathComponent.PathParameters(id: "my-ids")
66+
)
67+
let client = APIClient(mockSession: MockURLSession(requestHandler: { urlRequest in
68+
XCTAssertEqual(urlRequest.url?.pathComponents, ["/", "echo", "my-ids"])
69+
XCTAssertEqual(urlRequest.httpMethod, "POST")
70+
XCTAssertEqual(urlRequest.httpBody, try? JSONEncoder().encode("hello"))
71+
return (try? JSONEncoder().encode("world")) ?? Data()
72+
}))
73+
let response = try await client.request(endpoint)
74+
XCTAssertEqual(response, "world")
75+
}
76+
77+
}

0 commit comments

Comments
 (0)