import Foundation enum ProfileServiceError: LocalizedError { case unexpectedStatus(String) case decoding(debugDescription: String) case encoding(String) var errorDescription: String? { switch self { case .unexpectedStatus(let message): return message case .decoding(let debugDescription): return AppConfig.DEBUG ? debugDescription : NSLocalizedString("Не удалось загрузить профиль.", comment: "Profile service decoding error") case .encoding(let message): return message } } } final class ProfileService { private let client: NetworkClient private let decoder: JSONDecoder init(client: NetworkClient = .shared) { self.client = client self.decoder = JSONDecoder() self.decoder.keyDecodingStrategy = .convertFromSnakeCase self.decoder.dateDecodingStrategy = .custom(Self.decodeDate) } func fetchMyProfile(completion: @escaping (Result) -> Void) { client.request( path: "/v1/profile/me", method: .get, requiresAuth: true ) { [decoder] result in switch result { case .success(let response): do { let apiResponse = try decoder.decode(APIResponse.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Не удалось загрузить профиль.", comment: "Profile unexpected status") completion(.failure(ProfileServiceError.unexpectedStatus(message))) return } completion(.success(apiResponse.data)) } catch { let debugMessage = Self.describeDecodingError(error: error, data: response.data) if AppConfig.DEBUG { print("[ProfileService] decode profile failed: \(debugMessage)") } completion(.failure(ProfileServiceError.decoding(debugDescription: debugMessage))) } case .failure(let error): if case let NetworkError.server(_, data) = error, let data, let message = Self.errorMessage(from: data) { completion(.failure(ProfileServiceError.unexpectedStatus(message))) return } completion(.failure(error)) } } } func fetchMyProfile() async throws -> ProfileDataPayload { try await withCheckedThrowingContinuation { continuation in fetchMyProfile { result in continuation.resume(with: result) } } } func updateProfile(_ payload: ProfileUpdateRequestPayload, completion: @escaping (Result) -> Void) { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase print("payload \(payload)") guard let body = try? encoder.encode(payload) else { let message = NSLocalizedString("Не удалось подготовить данные запроса.", comment: "Profile update encoding error") completion(.failure(ProfileServiceError.encoding(message))) return } client.request( path: "/v1/profile/edit", method: .put, body: body, requiresAuth: true ) { result in switch result { case .success(let response): do { let decoder = JSONDecoder() decoder.keyDecodingStrategy = .convertFromSnakeCase let apiResponse = try decoder.decode(APIResponse.self, from: response.data) guard apiResponse.status == "fine" else { let message = apiResponse.detail ?? NSLocalizedString("Не удалось сохранить изменения профиля.", comment: "Profile update unexpected status") completion(.failure(ProfileServiceError.unexpectedStatus(message))) return } completion(.success(apiResponse.data.message)) } catch { let debugMessage = Self.describeDecodingError(error: error, data: response.data) if AppConfig.DEBUG { print("[ProfileService] decode update response failed: \(debugMessage)") } if AppConfig.DEBUG { completion(.failure(ProfileServiceError.decoding(debugDescription: debugMessage))) } else { let message = NSLocalizedString("Не удалось обработать ответ сервера.", comment: "Profile update decode error") completion(.failure(ProfileServiceError.unexpectedStatus(message))) } } case .failure(let error): if case let NetworkError.server(_, data) = error, let data, let message = Self.errorMessage(from: data) { completion(.failure(ProfileServiceError.unexpectedStatus(message))) return } completion(.failure(error)) } } } func updateProfile(_ payload: ProfileUpdateRequestPayload) async throws -> String { try await withCheckedThrowingContinuation { continuation in updateProfile(payload) { result in continuation.resume(with: result) } } } private static func decodeDate(from decoder: Decoder) throws -> Date { let container = try decoder.singleValueContainer() let string = try container.decode(String.self) if let date = iso8601WithFractionalSeconds.date(from: string) { return date } if let date = iso8601Simple.date(from: string) { return date } throw DecodingError.dataCorruptedError( in: container, debugDescription: "Невозможно декодировать дату: \(string)" ) } private static func describeDecodingError(error: Error, data: Data) -> String { var parts: [String] = [] if let decodingError = error as? DecodingError { parts.append(decodingDescription(from: decodingError)) } else { parts.append(error.localizedDescription) } if let payload = truncatedPayload(from: data) { parts.append("payload=\(payload)") } return parts.joined(separator: "\n") } private static func decodingDescription(from error: DecodingError) -> String { switch error { case .typeMismatch(let type, let context): return "Type mismatch for \(type) at \(codingPath(from: context)): \(context.debugDescription)" case .valueNotFound(let type, let context): return "Value not found for \(type) at \(codingPath(from: context)): \(context.debugDescription)" case .keyNotFound(let key, let context): return "Missing key '\(key.stringValue)' at \(codingPath(from: context)): \(context.debugDescription)" case .dataCorrupted(let context): return "Corrupted data at \(codingPath(from: context)): \(context.debugDescription)" @unknown default: return error.localizedDescription } } private static func codingPath(from context: DecodingError.Context) -> String { let path = context.codingPath.map { $0.stringValue }.filter { !$0.isEmpty } return path.isEmpty ? "root" : path.joined(separator: ".") } private static func truncatedPayload(from data: Data, limit: Int = 512) -> String? { guard let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines), !string.isEmpty else { return nil } if string.count <= limit { return string } let index = string.index(string.startIndex, offsetBy: limit) return String(string[string.startIndex.. String? { if let apiError = try? JSONDecoder().decode(ErrorResponse.self, from: data) { if let detail = apiError.detail, !detail.isEmpty { return detail } if let message = apiError.data?.message, !message.isEmpty { return message } } if let string = String(data: data, encoding: .utf8), !string.isEmpty { return string } return nil } private static let iso8601WithFractionalSeconds: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds] return formatter }() private static let iso8601Simple: ISO8601DateFormatter = { let formatter = ISO8601DateFormatter() formatter.formatOptions = [.withInternetDateTime] return formatter }() }