Skip to content

Instantly share code, notes, and snippets.

@joemasilotti
Last active March 25, 2021 21:29
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save joemasilotti/784dde13520f588071f30ce7b790cbc9 to your computer and use it in GitHub Desktop.
Save joemasilotti/784dde13520f588071f30ce7b790cbc9 to your computer and use it in GitHub Desktop.
Combine-powered HTTP client with success and failure JSON decoding
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
}
}
}
}
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()
}
}
}
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