Skip to content

Instantly share code, notes, and snippets.

@MaxenceMottard
Last active August 3, 2021 08:11
Show Gist options
  • Save MaxenceMottard/fd988d8ba21e139b86c3b3efb8efe527 to your computer and use it in GitHub Desktop.
Save MaxenceMottard/fd988d8ba21e139b86c3b3efb8efe527 to your computer and use it in GitHub Desktop.
Request Protocol with Swift & Combine
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)
}
}
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