Last active
March 25, 2021 21:29
-
-
Save joemasilotti/784dde13520f588071f30ce7b790cbc9 to your computer and use it in GitHub Desktop.
Combine-powered HTTP client with success and failure JSON decoding
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
enum HTTP { | |
struct Response<T> { | |
let value: T | |
let headers: [AnyHashable: Any] | |
} | |
enum HTTPError<T: LocalizedError>: LocalizedError { | |
case failedRequest | |
case invalidResponse | |
case invalidRequest(T) | |
var errorDescription: String? { | |
switch self { | |
case .failedRequest: return "The request failed." | |
case .invalidResponse: return "The response was invalid." | |
case let .invalidRequest(error): return error.localizedDescription | |
} | |
} | |
} | |
} |
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
extension HTTP { | |
struct Client { | |
func request<T, E>(_ request: URLRequest, success: T.Type, failure: E.Type) -> AnyPublisher<Response<T>, HTTPError<E>> where T: Decodable, E: Decodable { | |
URLSession.shared.dataTaskPublisher(for: request) | |
.tryMap { data, response -> (Data, HTTPURLResponse) in | |
guard let response = response as? HTTPURLResponse | |
else { throw HTTPError<E>.failedRequest } | |
return (data, response) | |
} | |
.tryMap { data, response -> (Data, HTTPURLResponse) in | |
if (200 ..< 300).contains(response.statusCode) { | |
return (data, response) | |
} else if let error = try? JSONDecoder().decode(E.self, from: data) { | |
throw HTTPError.invalidRequest(error) | |
} else { | |
throw HTTPError<E>.invalidResponse | |
} | |
} | |
.tryMap { data, response -> Response<T> in | |
guard let value = try? JSONDecoder().decode(T.self, from: data) | |
else { throw HTTPError<E>.invalidResponse } | |
return Response(value: value, headers: response.allHeaderFields) | |
} | |
.mapError { $0 as? HTTPError ?? .failedRequest } | |
.receive(on: DispatchQueue.main) | |
.eraseToAnyPublisher() | |
} | |
} | |
} |
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
struct Credentials: Encodable { | |
let email: String | |
let password: String | |
} | |
enum Auth { | |
struct Response: Decodable { | |
let status: String | |
let message: String | |
} | |
struct Error: Decodable, LocalizedError { | |
let status: String | |
let error: String | |
var errorDescription: String? { error } | |
} | |
} | |
class NewSessionViewModel: ObservableObject { | |
@Published var email: String = "" | |
@Published var password: String = "" | |
@Published var error: String? | |
var didFinish: (() -> Void)? | |
private let client = HTTP.Client() | |
private var cancellables = Set<AnyCancellable>() | |
func signIn() { | |
let credentials = Credentials(email: email, password: password) | |
let request = HTTP.BodyRequest(url: Endpoints.API.newSession, method: .post, body: credentials) | |
client.request(request, success: Auth.Response.self, failure: Auth.Error.self) | |
.sink { [weak self] completion in | |
if case let .failure(error) = completion { | |
self?.error = error.localizedDescription | |
} | |
} receiveValue: { [weak self] _ in | |
self?.didFinish?() | |
} | |
.store(in: &cancellables) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment