APIClient.swift 4.38 KB
//
//  APIClient.swift
//  hippo-watch Watch App
//

import Foundation

enum NetworkEnvironment: Int {
    case dev = 0
    case xlab = 1
    case release = 2

    static var current: NetworkEnvironment = .release

    var baseURL: URL {
        switch self {
        case .dev:
            return URL(string: "https://dev-api.doublefeel.cn")!
        case .xlab:
            return URL(string: "https://xlab-api.doublefeel.cn")!
        case .release:
            return URL(string: "https://api.doublefeel.cn")!
        }
    }
}

enum HTTPMethod: String {
    case get = "GET"
    case post = "POST"
}

enum ParameterEncoding {
    case url
    case json
}

protocol APIEndpoint {
    var path: String { get }
    var method: HTTPMethod { get }
    var parameters: [String: Any]? { get }
    var encoding: ParameterEncoding { get }
}

enum APIClientError: LocalizedError {
    case invalidURL
    case invalidResponse
    case badStatus(Int, String?)
    case emptyResponse

    var errorDescription: String? {
        switch self {
        case .invalidURL:
            return "接口地址无效"
        case .invalidResponse:
            return "接口响应无效"
        case .badStatus(let statusCode, let message):
            return message ?? "接口请求失败:\(statusCode)"
        case .emptyResponse:
            return "接口返回为空"
        }
    }
}

final class APIClient {
    static let shared = APIClient()

    private let session: URLSession
    private let decoder: JSONDecoder

    private init(session: URLSession = .shared) {
        self.session = session
        self.decoder = JSONDecoder()
        self.decoder.keyDecodingStrategy = .convertFromSnakeCase
    }

    func request<T: Decodable>(_ endpoint: APIEndpoint) async throws -> T {
        let request = try makeRequest(for: endpoint)
        let (data, response) = try await session.data(for: request)

        guard let httpResponse = response as? HTTPURLResponse else {
            throw APIClientError.invalidResponse
        }

        guard (200..<300).contains(httpResponse.statusCode) else {
            throw APIClientError.badStatus(httpResponse.statusCode, parseErrorMessage(from: data))
        }

        if T.self == EmptyResponse.self {
            return EmptyResponse() as! T
        }

        guard !data.isEmpty else {
            throw APIClientError.emptyResponse
        }

        return try decoder.decode(T.self, from: data)
    }

    private func makeRequest(for endpoint: APIEndpoint) throws -> URLRequest {
        guard var components = URLComponents(
            url: NetworkEnvironment.current.baseURL.appendingPathComponent(endpoint.path),
            resolvingAgainstBaseURL: false
        ) else {
            throw APIClientError.invalidURL
        }

        if endpoint.encoding == .url, let parameters = endpoint.parameters {
            components.queryItems = parameters.map { key, value in
                URLQueryItem(name: key, value: String(describing: value))
            }
        }

        guard let url = components.url else {
            throw APIClientError.invalidURL
        }

        var request = URLRequest(url: url)
        request.httpMethod = endpoint.method.rawValue
        request.timeoutInterval = 30
        request.setValue("application/json", forHTTPHeaderField: "Content-Type")
        request.setValue(WatchUserAgent.share.finalUA.isEmpty ? "doublefeel" : WatchUserAgent.share.finalUA, forHTTPHeaderField: "User-Agent")

        if let token = WatchUserinfoManager.share.token, !token.isEmpty {
            request.setValue(token, forHTTPHeaderField: "access_token")
        }

        if endpoint.encoding == .json, let parameters = endpoint.parameters {
            request.httpBody = try JSONSerialization.data(withJSONObject: parameters)
        }

        return request
    }

    private func parseErrorMessage(from data: Data) -> String? {
        guard !data.isEmpty,
              let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any] else {
            return nil
        }
        return object["error"] as? String
            ?? object["message"] as? String
            ?? object["detail"] as? String
    }
}

extension Error {
    func getToastErrorDescription() -> String {
        if let localizedError = self as? LocalizedError,
           let description = localizedError.errorDescription {
            return description
        }
        return localizedDescription
    }
}