Last active
August 3, 2021 08:11
-
-
Save MaxenceMottard/fd988d8ba21e139b86c3b3efb8efe527 to your computer and use it in GitHub Desktop.
Request Protocol with Swift & Combine
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
protocol FormURLEncodable { | |
var dictionary: [String: String] { get } | |
} | |
extension FormURLEncodable { | |
func encode() -> Data? { | |
return dictionary | |
.mapValues { $0.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? "" } | |
.map { "\($0)=\($1)" } | |
.joined(separator: "&") | |
.data(using: .utf8) | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Combine | |
protocol RequestProtocol { | |
var hostname: String { get } | |
} | |
extension RequestProtocol { | |
private var jsonDecoder: JSONDecoder { | |
return JSONDecoder() | |
} | |
private var jsonEncoder: JSONEncoder { | |
return JSONEncoder() | |
} | |
private var successStatusCodes: Set<Int> { | |
return Set<Int>(200...209) | |
} | |
var fixHeaders: [String: String] { | |
return ["Content-Type": "application/json"] | |
} | |
// MARK: Request Execution with Combine | |
typealias VoidResult = AnyPublisher<Void, Error> | |
typealias DecodedResult<T: Decodable> = AnyPublisher<T, Error> | |
typealias BeforeRequestFunction = (() -> VoidResult)? | |
func call<T: Decodable>(_ urlRequest: URLRequest, decodeType: T.Type, executeBefore: BeforeRequestFunction = nil) -> DecodedResult<T> { | |
if let executeBefore = executeBefore { | |
return executeBefore() | |
.flatMap { _ in | |
return call(urlRequest, decodeType: decodeType) | |
}.eraseToAnyPublisher() | |
} | |
return URLSession.shared.dataTaskPublisher(for: urlRequest) | |
.tryMap { [successStatusCodes] (data, response) -> Data in | |
if let response = response as? HTTPURLResponse, !successStatusCodes.contains(response.statusCode) { | |
throw NSError(domain: "SERVICE_ERROR", code: response.statusCode, userInfo: nil) | |
} | |
return data | |
} | |
.decode(type: T.self, decoder: jsonDecoder) | |
.receive(on: DispatchQueue.main) | |
.eraseToAnyPublisher() | |
} | |
func call(_ urlRequest: URLRequest, executeBefore: BeforeRequestFunction = nil) -> VoidResult { | |
if let executeBefore = executeBefore { | |
return executeBefore() | |
.flatMap { _ in | |
return call(urlRequest) | |
}.eraseToAnyPublisher() | |
} | |
return URLSession.shared.dataTaskPublisher(for: urlRequest) | |
.tryMap { [successStatusCodes] (data, response) -> Void in | |
if let response = response as? HTTPURLResponse, !successStatusCodes.contains(response.statusCode) { | |
throw NSError(domain: "SERVICE_ERROR", code: response.statusCode, userInfo: nil) | |
} | |
} | |
.receive(on: DispatchQueue.main) | |
.eraseToAnyPublisher() | |
} | |
// MARK: URLRequest Generation | |
func generateRequest<T: Encodable>(endpoint: String, method: HTTPMethod = .GET, | |
body: T, headers: [String: String] = [:]) -> Result<URLRequest, Error> { | |
guard var request = generateRequest(from: endpoint, method: method, headers: headers) else { | |
return .failure(NSError(domain: "URL_ERROR", code: 500, userInfo: nil)) | |
} | |
do { | |
request.httpBody = try jsonEncoder.encode(body) | |
} catch { | |
return .failure(NSError(domain: "URL_REQUEST_GENERETION_ERROR", code: 500, userInfo: nil)) | |
} | |
return .success(request) | |
} | |
func generateRequest<T: FormURLEncodable>(endpoint: String, method: HTTPMethod = .GET, | |
body: T, headers: [String: String] = [:]) -> Result<URLRequest, Error> { | |
guard var request = generateRequest(from: endpoint, method: method, headers: headers) else { | |
return .failure(NSError(domain: "URL_ERROR", code: 500, userInfo: nil)) | |
} | |
request.httpBody = body.encode() | |
return .success(request) | |
} | |
private func generateRequest(from endpoint: String, method: HTTPMethod, headers: [String: String]) -> URLRequest? { | |
guard let url = URL(string: "\(hostname)\(endpoint)") else { | |
return nil | |
} | |
var request = URLRequest(url: url) | |
request.httpMethod = method.rawValue | |
fixHeaders | |
.merging(headers) { _, dynamic in | |
return dynamic | |
}.forEach { | |
request.addValue($1, forHTTPHeaderField: $0) | |
} | |
return request | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment