Skip to content

Commit 28f479e

Browse files
authored
Improve Fake handling, add status code to faking with a file. (#280)
* Improve fake handling * Improve fake handling, add status code to faking with file * Remove dumb comparison * Rename none to data
1 parent 5aaa89f commit 28f479e

10 files changed

+201
-169
lines changed

Sources/Networking/JSON.swift

+13-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ enum ParsingError: Error {
66
}
77

88
enum JSON: Equatable {
9-
case none
9+
case data(Data)
1010

1111
case dictionary(Data, [String: Any])
1212

@@ -30,6 +30,17 @@ enum JSON: Equatable {
3030
}
3131
}
3232

33+
var data: Data {
34+
switch self {
35+
case let .dictionary(data, _):
36+
return data
37+
case let .array(data, _):
38+
return data
39+
case let .data(data):
40+
return data
41+
}
42+
}
43+
3344
init(_ data: Data) throws {
3445
let body = try JSONSerialization.jsonObject(with: data, options: [])
3546

@@ -38,7 +49,7 @@ enum JSON: Equatable {
3849
} else if let array = body as? [[String: Any]] {
3950
self = .array(data, array)
4051
} else {
41-
self = JSON.none
52+
self = .data(data)
4253
}
4354
}
4455

Sources/Networking/Networking+HTTPRequests.swift

+10-10
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@ public extension Networking {
2929
/// - path: The path for the faked GET request.
3030
/// - fileName: The name of the file, whose contents will be registered as a reponse.
3131
/// - bundle: The Bundle where the file is located.
32-
func fakeGET(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) {
33-
registerFake(requestType: .get, path: path, fileName: fileName, bundle: bundle, delay: delay)
32+
func fakeGET(_ path: String, fileName: String, bundle: Bundle = Bundle.main, statusCode: Int = 200, delay: Double = 0) {
33+
registerFake(requestType: .get, path: path, fileName: fileName, bundle: bundle, statusCode: statusCode, delay: delay)
3434
}
3535

3636
/// Cancels the GET request for the specified path. This causes the request to complete with error code URLError.cancelled.
@@ -127,8 +127,8 @@ public extension Networking {
127127
/// - path: The path for the faked PATCH request.
128128
/// - fileName: The name of the file, whose contents will be registered as a reponse.
129129
/// - bundle: The Bundle where the file is located.
130-
func fakePATCH(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) {
131-
registerFake(requestType: .patch, path: path, fileName: fileName, bundle: bundle, delay: delay)
130+
func fakePATCH(_ path: String, fileName: String, bundle: Bundle = Bundle.main, statusCode: Int = 200, delay: Double = 0) {
131+
registerFake(requestType: .patch, path: path, fileName: fileName, bundle: bundle, statusCode: statusCode, delay: delay)
132132
}
133133

134134
/// Cancels the PATCH request for the specified path. This causes the request to complete with error code URLError.cancelled.
@@ -169,8 +169,8 @@ public extension Networking {
169169
/// - path: The path for the faked PUT request.
170170
/// - fileName: The name of the file, whose contents will be registered as a reponse.
171171
/// - bundle: The Bundle where the file is located.
172-
func fakePUT(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) {
173-
registerFake(requestType: .put, path: path, fileName: fileName, bundle: bundle, delay: delay)
172+
func fakePUT(_ path: String, fileName: String, bundle: Bundle = Bundle.main, statusCode: Int = 200, delay: Double = 0) {
173+
registerFake(requestType: .put, path: path, fileName: fileName, bundle: bundle, statusCode: statusCode, delay: delay)
174174
}
175175

176176
/// Cancels the PUT request for the specified path. This causes the request to complete with error code URLError.cancelled.
@@ -221,8 +221,8 @@ public extension Networking {
221221
/// - path: The path for the faked POST request.
222222
/// - fileName: The name of the file, whose contents will be registered as a reponse.
223223
/// - bundle: The Bundle where the file is located.
224-
func fakePOST(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) {
225-
registerFake(requestType: .post, path: path, fileName: fileName, bundle: bundle, delay: delay)
224+
func fakePOST(_ path: String, fileName: String, bundle: Bundle = Bundle.main, statusCode: Int = 200, delay: Double = 0) {
225+
registerFake(requestType: .post, path: path, fileName: fileName, bundle: bundle, statusCode: statusCode, delay: delay)
226226
}
227227

228228
/// Cancels the POST request for the specified path. This causes the request to complete with error code URLError.cancelled.
@@ -263,8 +263,8 @@ public extension Networking {
263263
/// - path: The path for the faked DELETE request.
264264
/// - fileName: The name of the file, whose contents will be registered as a reponse.
265265
/// - bundle: The Bundle where the file is located.
266-
func fakeDELETE(_ path: String, fileName: String, bundle: Bundle = Bundle.main, delay: Double = 0) {
267-
registerFake(requestType: .delete, path: path, fileName: fileName, bundle: bundle, delay: delay)
266+
func fakeDELETE(_ path: String, fileName: String, bundle: Bundle = Bundle.main, statusCode: Int = 200, delay: Double = 0) {
267+
registerFake(requestType: .delete, path: path, fileName: fileName, bundle: bundle, statusCode: statusCode, delay: delay)
268268
}
269269

270270
/// Cancels the DELETE request for the specified path. This causes the request to complete with error code URLError.cancelled.

Sources/Networking/Networking+New.swift

+97-107
Original file line numberDiff line numberDiff line change
@@ -2,122 +2,112 @@ import Foundation
22

33
extension Networking {
44
func handle<T: Decodable>(_ requestType: RequestType, path: String, parameters: Any?) async -> Result<T, NetworkingError> {
5-
var data: Data?
65
do {
76
logger.info("Starting \(requestType.rawValue) request to \(path, privacy: .public)")
87

98
if let fakeRequest = try FakeRequest.find(ofType: requestType, forPath: path, in: fakeRequests) {
10-
let (_, response, error) = try handleFakeRequest(fakeRequest, path: path, cacheName: nil, cachingLevel: .none)
11-
if fakeRequest.delay > 0 {
12-
let nanoseconds = UInt64(fakeRequest.delay * 1_000_000_000)
13-
try? await Task.sleep(nanoseconds: nanoseconds)
14-
}
15-
let result = try JSONResult(body: fakeRequest.response, response: response, error: error)
16-
switch result {
17-
case .success(let response):
18-
if T.self == Data.self {
19-
logger.info("Successfully processed fake request to \(path, privacy: .public)")
20-
return .success(Data() as! T)
21-
} else if T.self == NetworkingResponse.self {
22-
let headers = Dictionary(uniqueKeysWithValues: response.headers.compactMap { key, value in
23-
(key as? String).map { ($0, AnyCodable(value)) }
24-
})
25-
let body = try JSONDecoder().decode([String: AnyCodable].self, from: response.data)
26-
let networkingJSON = NetworkingResponse(headers: headers, body: body)
27-
return .success(networkingJSON as! T)
28-
} else {
29-
let decoder = JSONDecoder()
30-
decoder.dateDecodingStrategy = .iso8601
31-
let decodedResponse = try decoder.decode(T.self, from: response.data)
32-
logger.info("Successfully decoded response from fake request to \(path, privacy: .public)")
33-
return .success(decodedResponse)
34-
}
35-
case .failure(let response):
36-
logger.error("Failed to process fake request to \(path, privacy: .public): \(response.error.localizedDescription, privacy: .public)")
37-
return .failure(.unexpectedError(statusCode: nil, message: "Failed to process fake request (error: \(response.error.localizedDescription))."))
38-
}
9+
return try await handleFakeRequest(fakeRequest, path: path, requestType: requestType)
3910
}
4011

41-
let parameterType: Networking.ParameterType? = parameters != nil ? .json : nil
42-
var request = URLRequest(url: try composedURL(with: path), requestType: requestType, path: path, parameterType: parameterType, responseType: .json, boundary: boundary, authorizationHeaderValue: authorizationHeaderValue, token: token, authorizationHeaderKey: authorizationHeaderKey, headerFields: headerFields)
12+
let request = try createRequest(path: path, requestType: requestType, parameters: parameters)
13+
let (responseData, response) = try await session.data(for: request)
14+
return try handleResponse(responseData: responseData, response: response, path: path)
4315

44-
if let parameters = parameters {
45-
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
46-
}
16+
} catch {
17+
return handleRequestError(error: error)
18+
}
19+
}
4720

48-
let (responseData, response) = try await session.data(for: request)
49-
data = responseData
50-
guard let httpResponse = response as? HTTPURLResponse else {
51-
logger.error("Invalid response received from \(path, privacy: .public)")
52-
return .failure(.invalidResponse)
53-
}
21+
private func handleFakeRequest<T: Decodable>(_ fakeRequest: FakeRequest, path: String, requestType: RequestType) async throws -> Result<T, NetworkingError> {
22+
let (_, response, error) = try handleFakeRequest(fakeRequest, path: path, cacheName: nil, cachingLevel: .none)
5423

55-
let statusCode = httpResponse.statusCode
56-
switch statusCode.statusCodeType {
57-
case .informational, .successful:
58-
logger.info("Received successful response with status code \(statusCode) from \(path, privacy: .public)")
59-
if T.self == Data.self {
60-
return .success(Data() as! T)
61-
} else if T.self == NetworkingResponse.self {
62-
let headers = Dictionary(uniqueKeysWithValues: httpResponse.allHeaderFields.compactMap { key, value in
63-
(key as? String).map { ($0, AnyCodable(value)) }
64-
})
65-
let body = try JSONDecoder().decode([String: AnyCodable].self, from: responseData)
66-
let networkingJSON = NetworkingResponse(headers: headers, body: body)
67-
return .success(networkingJSON as! T)
68-
} else {
69-
let decoder = JSONDecoder()
70-
decoder.dateDecodingStrategy = .iso8601
71-
let decodedResponse = try decoder.decode(T.self, from: responseData)
72-
return .success(decodedResponse)
73-
}
74-
case .redirection:
75-
logger.warning("Redirection response with status code \(statusCode) from \(path, privacy: .public)")
76-
return .failure(.unexpectedError(statusCode: statusCode, message: "Redirection occurred."))
77-
case .clientError:
78-
let errorMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode)
79-
if let jsonString = String(data: responseData, encoding: .utf8) {
80-
logger.warning("Client error: \(jsonString, privacy: .public) with status code \(statusCode) from \(path, privacy: .public)")
81-
}
82-
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: responseData) {
83-
logger.warning("Client error: \(errorResponse.combinedMessage, privacy: .public) with status code \(statusCode) from \(path, privacy: .public)")
84-
return .failure(.clientError(statusCode: statusCode, message: errorResponse.combinedMessage))
85-
} else {
86-
logger.warning("Client error: \(errorMessage, privacy: .public) with status code \(statusCode) from \(path, privacy: .public)")
87-
return .failure(.clientError(statusCode: statusCode, message: errorMessage))
88-
}
89-
case .serverError:
90-
let errorMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode)
91-
var errorDetails: [String: Any]? = nil
92-
if let jsonString = String(data: responseData, encoding: .utf8) {
93-
logger.error("Server error: \(jsonString, privacy: .public) with status code \(statusCode) from \(path, privacy: .public)")
94-
}
95-
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: responseData) {
96-
errorDetails = ["error": errorResponse.error ?? "",
97-
"message": errorResponse.message ?? "",
98-
"errors": errorResponse.errors ?? [:]]
99-
logger.error("Server error: \(errorResponse.combinedMessage, privacy: .public) with status code \(statusCode) from \(path, privacy: .public)")
100-
return .failure(.serverError(statusCode: statusCode, message: errorResponse.combinedMessage, details: errorDetails))
101-
} else {
102-
logger.error("Server error: \(errorMessage, privacy: .public) with status code \(statusCode) from \(path, privacy: .public)")
103-
return .failure(.serverError(statusCode: statusCode, message: errorMessage, details: errorDetails))
104-
}
105-
case .cancelled:
106-
logger.info("Request cancelled with status code \(statusCode) from \(path, privacy: .public)")
107-
return .failure(.unexpectedError(statusCode: statusCode, message: "Request was cancelled."))
108-
case .unknown:
109-
logger.error("Unexpected error with status code \(statusCode) from \(path, privacy: .public)")
110-
return .failure(.unexpectedError(statusCode: statusCode, message: "An unexpected error occurred."))
111-
}
112-
} catch let error as NSError {
113-
if let data = data, let jsonString = String(data: data, encoding: .utf8) {
114-
logger.error("Unexpected error occurred: \(error.localizedDescription, privacy: .public). Response data: \(jsonString, privacy: .public)")
115-
} else if let decodingError = error as? DecodingError {
116-
logger.error("Unexpected error occurred: \(decodingError.detailedMessage, privacy: .public)")
117-
} else {
118-
logger.error("Unexpected error occurred: \(error.localizedDescription, privacy: .public)")
119-
}
120-
return .failure(.unexpectedError(statusCode: nil, message: "Failed to process request (error: \(error.localizedDescription))."))
24+
if fakeRequest.delay > 0 {
25+
try? await Task.sleep(nanoseconds: UInt64(fakeRequest.delay * 1_000_000_000))
26+
}
27+
28+
let result = try JSONResult(body: fakeRequest.response, response: response, error: error)
29+
return try handleResponse(responseData: result.data, response: response, path: path)
30+
}
31+
32+
private func createRequest(path: String, requestType: RequestType, parameters: Any?) throws -> URLRequest {
33+
let parameterType: Networking.ParameterType? = parameters != nil ? .json : nil
34+
var request = URLRequest(url: try composedURL(with: path), requestType: requestType, path: path, parameterType: parameterType, responseType: .json, boundary: boundary, authorizationHeaderValue: authorizationHeaderValue, token: token, authorizationHeaderKey: authorizationHeaderKey, headerFields: headerFields)
35+
36+
if let parameters = parameters {
37+
request.httpBody = try JSONSerialization.data(withJSONObject: parameters, options: [])
38+
}
39+
40+
return request
41+
}
42+
43+
private func handleResponse<T: Decodable>(responseData: Data, response: URLResponse, path: String) throws -> Result<T, NetworkingError> {
44+
guard let httpResponse = response as? HTTPURLResponse else {
45+
return .failure(.invalidResponse)
46+
}
47+
48+
let statusCode = httpResponse.statusCode
49+
switch statusCode.statusCodeType {
50+
case .informational, .successful:
51+
return try handleSuccessfulResponse(responseData: responseData, path: path, httpResponse: httpResponse)
52+
case .redirection:
53+
return .failure(.unexpectedError(statusCode: statusCode, message: "Redirection occurred."))
54+
case .clientError:
55+
return try handleClientError(responseData: responseData, statusCode: statusCode, path: path)
56+
case .serverError:
57+
return try handleServerError(responseData: responseData, statusCode: statusCode, path: path)
58+
case .cancelled:
59+
return .failure(.unexpectedError(statusCode: statusCode, message: "Request was cancelled."))
60+
case .unknown:
61+
return .failure(.unexpectedError(statusCode: statusCode, message: "An unexpected error occurred."))
62+
}
63+
}
64+
65+
private func handleSuccessfulResponse<T: Decodable>(responseData: Data, path: String, httpResponse: HTTPURLResponse) throws -> Result<T, NetworkingError> {
66+
if T.self == Data.self {
67+
return .success(Data() as! T)
68+
} else if T.self == NetworkingResponse.self {
69+
let headers = Dictionary(uniqueKeysWithValues: httpResponse.allHeaderFields.compactMap { key, value in
70+
(key as? String).map { ($0, AnyCodable(value)) }
71+
})
72+
let body = try JSONDecoder().decode([String: AnyCodable].self, from: responseData)
73+
let networkingJSON = NetworkingResponse(headers: headers, body: body)
74+
return .success(networkingJSON as! T)
75+
} else {
76+
let decoder = JSONDecoder()
77+
decoder.dateDecodingStrategy = .iso8601
78+
let decodedResponse = try decoder.decode(T.self, from: responseData)
79+
return .success(decodedResponse)
80+
}
81+
}
82+
83+
private func handleClientError<T: Decodable>(responseData: Data, statusCode: Int, path: String) throws -> Result<T, NetworkingError> {
84+
let errorMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode)
85+
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: responseData) {
86+
return .failure(.clientError(statusCode: statusCode, message: errorResponse.combinedMessage))
87+
} else {
88+
return .failure(.clientError(statusCode: statusCode, message: errorMessage))
89+
}
90+
}
91+
92+
private func handleServerError<T: Decodable>(responseData: Data, statusCode: Int, path: String) throws -> Result<T, NetworkingError> {
93+
let errorMessage = HTTPURLResponse.localizedString(forStatusCode: statusCode)
94+
var errorDetails: [String: Any]? = nil
95+
if let errorResponse = try? JSONDecoder().decode(ErrorResponse.self, from: responseData) {
96+
errorDetails = ["error": errorResponse.error ?? "",
97+
"message": errorResponse.message ?? "",
98+
"errors": errorResponse.errors ?? [:]]
99+
return .failure(.serverError(statusCode: statusCode, message: errorResponse.combinedMessage, details: errorDetails))
100+
} else {
101+
return .failure(.serverError(statusCode: statusCode, message: errorMessage, details: errorDetails))
102+
}
103+
}
104+
105+
private func handleRequestError<T: Decodable>(error: Error) -> Result<T, NetworkingError> {
106+
if let decodingError = error as? DecodingError {
107+
logger.error("Unexpected error occurred: \(decodingError.detailedMessage, privacy: .public)")
108+
} else {
109+
logger.error("Unexpected error occurred: \(error.localizedDescription, privacy: .public)")
121110
}
111+
return .failure(.unexpectedError(statusCode: nil, message: "Failed to process request (error: \(error.localizedDescription))."))
122112
}
123113
}

0 commit comments

Comments
 (0)