Skip to content

Commit a557868

Browse files
authored
JSONDecoder at Client level (#6)
1 parent 3df9e1b commit a557868

File tree

2 files changed

+100
-30
lines changed

2 files changed

+100
-30
lines changed

Sources/URLRouting/Client/Client.swift

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,14 @@ import XCTestDynamicOverlay
1616
/// responses.
1717
public struct URLRoutingClient<Route> {
1818
var request: (Route) async throws -> (Data, URLResponse)
19+
let decoder: JSONDecoder
1920

20-
public init(request: @escaping (Route) async throws -> (Data, URLResponse)) {
21+
public init(
22+
request: @escaping (Route) async throws -> (Data, URLResponse),
23+
decoder: JSONDecoder = .init()
24+
) {
2125
self.request = request
26+
self.decoder = decoder
2227
}
2328

2429
/// Makes a request to a route.
@@ -32,11 +37,11 @@ public struct URLRoutingClient<Route> {
3237
public func request<Value: Decodable>(
3338
_ route: Route,
3439
as type: Value.Type = Value.self,
35-
decoder: JSONDecoder = .init()
40+
decoder: JSONDecoder? = nil
3641
) async throws -> (value: Value, response: URLResponse) {
3742
let (data, response) = try await self.request(route)
3843
do {
39-
return (try decoder.decode(type, from: data), response)
44+
return (try (decoder ?? self.decoder).decode(type, from: data), response)
4045
} catch {
4146
throw URLRoutingDecodingError(bytes: data, response: response, underlyingError: error)
4247
}
@@ -60,39 +65,46 @@ extension URLRoutingClient {
6065
/// - session: A URL session.
6166
/// - Returns: A live API client that makes requests through a URL session.
6267
@available(macOS 10.15, iOS 13, watchOS 6, tvOS 13, *)
63-
public static func live<R: ParserPrinter>(router: R, session: URLSession = .shared) -> Self
68+
public static func live<R: ParserPrinter>(
69+
router: R,
70+
session: URLSession = .shared,
71+
decoder: JSONDecoder = .init()
72+
) -> Self
6473
where R.Input == URLRequestData, R.Output == Route {
65-
Self { route in
66-
let request = try router.request(for: route)
74+
Self.init(
75+
request: { route in
76+
let request = try router.request(for: route)
6777

68-
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
69-
if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
70-
return try await session.data(for: request)
71-
}
72-
#endif
73-
var dataTask: URLSessionDataTask?
74-
let cancel: () -> Void = { dataTask?.cancel() }
78+
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
79+
if #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) {
80+
return try await session.data(for: request)
81+
}
82+
#endif
83+
var dataTask: URLSessionDataTask?
84+
let cancel: () -> Void = { dataTask?.cancel() }
7585

76-
return try await withTaskCancellationHandler(
77-
handler: { cancel() },
78-
operation: {
79-
try await withCheckedThrowingContinuation { continuation in
80-
dataTask = session.dataTask(with: request) { data, response, error in
81-
guard
82-
let data = data,
83-
let response = response
84-
else {
85-
continuation.resume(throwing: error ?? URLError(.badServerResponse))
86-
return
87-
}
86+
return try await withTaskCancellationHandler(
87+
handler: { cancel() },
88+
operation: {
89+
try await withCheckedThrowingContinuation { continuation in
90+
dataTask = session.dataTask(with: request) { data, response, error in
91+
guard
92+
let data = data,
93+
let response = response
94+
else {
95+
continuation.resume(throwing: error ?? URLError(.badServerResponse))
96+
return
97+
}
8898

89-
continuation.resume(returning: (data, response))
99+
continuation.resume(returning: (data, response))
100+
}
101+
dataTask?.resume()
90102
}
91-
dataTask?.resume()
92103
}
93-
}
94-
)
95-
}
104+
)
105+
},
106+
decoder: decoder
107+
)
96108
}
97109
}
98110

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import Parsing
2+
import URLRouting
3+
import XCTest
4+
5+
#if canImport(FoundationNetworking)
6+
import FoundationNetworking
7+
#endif
8+
9+
class URLRoutingClientTests: XCTestCase {
10+
#if os(macOS) || os(iOS) || os(tvOS) || os(watchOS)
11+
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
12+
func testJSONDecoder_noDecoder() async throws {
13+
struct Response: Equatable, Decodable {
14+
let decodableValue: String
15+
}
16+
enum AppRoute {
17+
case test
18+
}
19+
let sut = URLRoutingClient<AppRoute>(request: { _ in
20+
("{\"decodableValue\":\"result\"}".data(using: .utf8)!, URLResponse())
21+
})
22+
let response = try await sut.request(.test, as: Response.self)
23+
XCTAssertEqual(response.value, .init(decodableValue: "result"))
24+
}
25+
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
26+
func testJSONDecoder_customDecoder() async throws {
27+
struct Response: Equatable, Decodable {
28+
let decodableValue: String
29+
}
30+
enum AppRoute {
31+
case test
32+
}
33+
let customDecoder = JSONDecoder()
34+
customDecoder.keyDecodingStrategy = .convertFromSnakeCase
35+
let sut = URLRoutingClient<AppRoute>(request: { _ in
36+
("{\"decodable_value\":\"result\"}".data(using: .utf8)!, URLResponse())
37+
}, decoder: customDecoder)
38+
let response = try await sut.request(.test, as: Response.self)
39+
XCTAssertEqual(response.value, .init(decodableValue: "result"))
40+
}
41+
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
42+
func testJSONDecoder_customDecoderForRequest() async throws {
43+
struct Response: Equatable, Decodable {
44+
let decodableValue: String
45+
}
46+
enum AppRoute {
47+
case test
48+
}
49+
let customDecoder = JSONDecoder()
50+
customDecoder.keyDecodingStrategy = .convertFromSnakeCase
51+
let sut = URLRoutingClient<AppRoute>(request: { _ in
52+
("{\"decodableValue\":\"result\"}".data(using: .utf8)!, URLResponse())
53+
}, decoder: customDecoder)
54+
let response = try await sut.request(.test, as: Response.self, decoder: .init())
55+
XCTAssertEqual(response.value, .init(decodableValue: "result"))
56+
}
57+
#endif
58+
}

0 commit comments

Comments
 (0)