holbalzs / networkrefactor

51 stars 23 forks source link

code from Essential Developer video #1

Open bootuz opened 1 year ago

bootuz commented 1 year ago

Hello there! By any chance do you have the code from the Essential Developer video?

MrEldin commented 7 months ago

Here is the code, I wrote from the video if someone needs it.

import Foundation
import Combine

protocol HTTPClient {
    func publisher(request: URLRequest) -> AnyPublisher<(Data, HTTPURLResponse), Error>
}

extension URLSession: HTTPClient {
    struct InvalidHTTPResponseError: Error {}

    func publisher(request: URLRequest) -> AnyPublisher<(Data, HTTPURLResponse), any Error> {
        return dataTaskPublisher(for: request)
            .tryMap({ result in
                guard let httpResponse = result.response as? HTTPURLResponse else {
                    throw InvalidHTTPResponseError()
                }
                return (result.data, httpResponse)
            })
            .eraseToAnyPublisher()
    }
}

protocol TokenProvider {
    func tokenPublisher() -> AnyPublisher<AuthenticationJWTDTO, Error>
}

//enum LoginState {
//    case loggedIn
//    case loggedOut
//}
//class ViewState: ObservableObject {
//    @Published var loginState: LoginState
//    
//    func logout() {
//        loginState = .loggedOut
//    }
//}

class CountryListService {
    let httpClient: HTTPClient

    init(httpClient: HTTPClient) {
        self.httpClient = httpClient
    }

    func loadAllCountries() -> AnyPublisher<Root<LegalCase>, Error> {
        return httpClient
            .publisher(request: LanguageCountryProvider.getCountries.makeRequest)
            .tryMap(GenericAPIHTTPRequestMapper.map)
            .eraseToAnyPublisher()
    }
}

private let customDateJSONDecoder: JSONDecoder = {
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .custom(customDateDecodingStrategy)
    return decoder
}()

public func customDateDecodingStrategy(decoder: Decoder) throws -> Date {
    let container = try decoder.singleValueContainer()
    let str = try container.decode(String.self)
    return try Date.dateFromString(str)
}

struct GenericAPIHTTPRequestMapper {
    static func map<T>(data: Data, response: HTTPURLResponse) throws -> T where T: Decodable {
        if(200..<300) ~= response.statusCode {
            return try customDateJSONDecoder.decode(T.self, from: data)
        } else if response.statusCode == 401 {
            throw APIErrorHandler.tokenExpired
        } else {
            if let error = try? JSONDecoder().decode(ApiErrorDTO.self, from: data) {
                throw APIErrorHandler.customApiError(error)
            } else {
                throw APIErrorHandler.emptyErrorWithStatusCode(response.statusCode.description)
            }
        }
    }
}

class AuthenticatedHTTPClientDecorator: HTTPClient {
    let client: HTTPClient
    let tokenProvider: TokenProvider
    var needsAuth: (() -> Void)?

    init(client: HTTPClient, tokenProvider: TokenProvider) {
        self.client = client
        self.tokenProvider = tokenProvider
    }

    func publisher(request: URLRequest) -> AnyPublisher<(Data, HTTPURLResponse), any Error> {
        return tokenProvider
            .tokenPublisher()
            .map{ token in
                var signedRequest = request
                signedRequest.allHTTPHeaderFields?.removeValue(forKey: "Authorization")
                signedRequest.addValue("Bearer \(token.accessToken)", forHTTPHeaderField: "Authorization")

                return signedRequest
            }
            .flatMap(client.publisher)
            .handleEvents(receiveCompletion: { [needsAuth] completion in
                if case let Subscribers.Completion<Error>.failure(error) = completion, case APIErrorHandler.tokenExpired? = error as? APIErrorHandler {
                    needsAuth?()
                }
            })
            .eraseToAnyPublisher()
    }
}

internal extension Date {

    enum DateParserError: Error {
        case failedToParseDateFromString(String)
        case typeUnhandled(Any?)
    }

    // MARK: - Class

    static func dateFromString(_ string: Any?) throws -> Date {
        if let dateString = string as? String {
            let count = dateString.count
            if count <= 10 {
                ISO8601DateFormatter.dateFormat = "yyyy-MM-dd"
            } else if count == 23 {
                ISO8601DateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss ZZZ"
            } else if count == 19 {
                ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss"
            } else if count > 23 && dateString.contains("+") {
                ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
            } else {
                ISO8601DateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS"
            }

            if let date = ISO8601DateFormatter.date(from: dateString) {
                return date
            } else {
                throw DateParserError.failedToParseDateFromString("String to parse: \(dateString), date format: \(String(describing: ISO8601DateFormatter.dateFormat))")
            }
        } else if let date = string as? Date {
            return date
        } else {
            throw DateParserError.typeUnhandled(string)
        }
    }
}

private let ISO8601DateFormatter: DateFormatter = {
    let dateFormatter = DateFormatter()
    let enUSPOSIXLocale = Locale(identifier: "en_US_POSIX")
    dateFormatter.locale = enUSPOSIXLocale
    dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
    return dateFormatter
}()
firdavsbagirov commented 5 months ago

Hello there, did anyone complete 'refreshToken' logic? how to handle 401 from any request, refresh the access token, and resend the request again? In a mentoring session only 401 from refresh token was handled, as I understood