Not has address
This commit is contained in:
@@ -2,6 +2,9 @@ import Foundation
|
||||
#if canImport(FoundationNetworking)
|
||||
import FoundationNetworking
|
||||
#endif
|
||||
#if canImport(LCEssentials) && os(iOS)
|
||||
import LCEssentials
|
||||
#endif
|
||||
|
||||
enum NetworkError: Error, LocalizedError {
|
||||
case invalidURL
|
||||
@@ -36,7 +39,7 @@ enum NetworkError: Error, LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
struct ApiRequest {
|
||||
struct ApiRequest: Sendable {
|
||||
let path: String
|
||||
let method: String
|
||||
let module: ApiModule
|
||||
@@ -71,18 +74,117 @@ final class ApiClient {
|
||||
}
|
||||
|
||||
func send<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||
#if canImport(LCEssentials) && os(iOS)
|
||||
return try await sendWithLCEssentials(request)
|
||||
#else
|
||||
return try await sendWithURLSession(request)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
private extension ApiClient {
|
||||
#if canImport(LCEssentials) && os(iOS)
|
||||
func sendWithLCEssentials<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||
let urlString = try buildURL(path: request.path, query: request.queryItems).absoluteString
|
||||
let method = request.method
|
||||
let headers = buildHeaders(for: request)
|
||||
let params = request.body
|
||||
|
||||
var attempt = 1
|
||||
while attempt <= maxAttempts {
|
||||
do {
|
||||
let responseString = try await Self.performLCERequest(
|
||||
url: urlString,
|
||||
params: params,
|
||||
method: method,
|
||||
headers: headers
|
||||
)
|
||||
guard let data = responseString.data(using: .utf8) else {
|
||||
throw NetworkError.decodeError("Resposta nao UTF-8")
|
||||
}
|
||||
do {
|
||||
return try JSONDecoder().decode(T.self, from: data)
|
||||
} catch {
|
||||
throw NetworkError.decodeError(String(data: data, encoding: .utf8))
|
||||
}
|
||||
} catch {
|
||||
let mapped = mapError(error)
|
||||
guard shouldRetry(mapped), attempt < maxAttempts else {
|
||||
throw mapped
|
||||
}
|
||||
try await Task.sleep(nanoseconds: backoff(for: attempt, error: mapped))
|
||||
attempt += 1
|
||||
}
|
||||
}
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
@MainActor
|
||||
static func performLCERequest(
|
||||
url: String,
|
||||
params: Data?,
|
||||
method: String,
|
||||
headers: [String: String]
|
||||
) async throws -> String {
|
||||
let httpMethod = toHTTPMethod(method)
|
||||
return try await API.shared.request(
|
||||
url: url,
|
||||
params: params,
|
||||
method: httpMethod,
|
||||
headers: headers,
|
||||
jsonEncoding: true,
|
||||
debug: true
|
||||
)
|
||||
}
|
||||
|
||||
static func toHTTPMethod(_ method: String) -> httpMethod {
|
||||
switch method.uppercased() {
|
||||
case "POST": return .post
|
||||
case "PUT": return .put
|
||||
case "DELETE": return .delete
|
||||
default: return .get
|
||||
}
|
||||
}
|
||||
|
||||
func mapError(_ error: Error) -> NetworkError {
|
||||
printError(title: "httpReqError", msg: error.localizedDescription)
|
||||
if let network = error as? NetworkError {
|
||||
return network
|
||||
}
|
||||
if let decoding = error as? DecodingError {
|
||||
return .decodeError(String(describing: decoding))
|
||||
}
|
||||
|
||||
let nsError = error as NSError
|
||||
let apiMessage = serverMessage(from: nsError)
|
||||
|
||||
switch nsError.code {
|
||||
case 401, 403:
|
||||
return .unauthorized(apiMessage)
|
||||
case 429:
|
||||
return .rateLimited(nil)
|
||||
case 400...599:
|
||||
return .httpError(nsError.code, apiMessage)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
if nsError.domain == NSURLErrorDomain {
|
||||
return .transportError(nsError.localizedDescription)
|
||||
}
|
||||
|
||||
return .transportError(nsError.localizedDescription)
|
||||
}
|
||||
#endif
|
||||
|
||||
func sendWithURLSession<T: Decodable>(_ request: ApiRequest) async throws -> T {
|
||||
let url = try buildURL(path: request.path, query: request.queryItems)
|
||||
var urlRequest = URLRequest(url: url)
|
||||
urlRequest.httpMethod = request.method
|
||||
urlRequest.httpBody = request.body
|
||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Accept")
|
||||
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||
|
||||
if let token = ApiConfig.token(for: request.module) {
|
||||
urlRequest.setValue(token, forHTTPHeaderField: "Atomenta-Token")
|
||||
}
|
||||
if request.requiresAuth, let jwt = tokenStore.jwt {
|
||||
urlRequest.setValue("Bearer \(jwt)", forHTTPHeaderField: "Authorization")
|
||||
for (key, value) in buildHeaders(for: request) {
|
||||
urlRequest.setValue(value, forHTTPHeaderField: key)
|
||||
}
|
||||
|
||||
var attempt = 1
|
||||
@@ -105,7 +207,7 @@ final class ApiClient {
|
||||
throw NetworkError.invalidResponse
|
||||
}
|
||||
|
||||
private func perform<T: Decodable>(_ request: URLRequest, as type: T.Type) async throws -> T {
|
||||
func perform<T: Decodable>(_ request: URLRequest, as type: T.Type) async throws -> T {
|
||||
let (data, response): (Data, URLResponse)
|
||||
do {
|
||||
(data, response) = try await session.data(for: request)
|
||||
@@ -137,7 +239,22 @@ final class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func shouldRetry(_ error: NetworkError) -> Bool {
|
||||
func buildHeaders(for request: ApiRequest) -> [String: String] {
|
||||
var headers: [String: String] = [
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
]
|
||||
|
||||
if let token = ApiConfig.token(for: request.module) {
|
||||
headers["Atomenta-Token"] = token
|
||||
}
|
||||
if request.requiresAuth, let jwt = tokenStore.jwt {
|
||||
headers["Authorization"] = "Bearer \(jwt)"
|
||||
}
|
||||
return headers
|
||||
}
|
||||
|
||||
func shouldRetry(_ error: NetworkError) -> Bool {
|
||||
switch error {
|
||||
case .rateLimited, .transportError:
|
||||
return true
|
||||
@@ -148,7 +265,7 @@ final class ApiClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func backoff(for attempt: Int, error: NetworkError) -> UInt64 {
|
||||
func backoff(for attempt: Int, error: NetworkError) -> UInt64 {
|
||||
if case .rateLimited(let retryAfter) = error, let retryAfter {
|
||||
return UInt64(retryAfter) * 1_000_000_000
|
||||
}
|
||||
@@ -156,7 +273,7 @@ final class ApiClient {
|
||||
return min(baseBackoffNanoseconds * multiplier, 2_000_000_000)
|
||||
}
|
||||
|
||||
private func serverMessage(from data: Data) -> String? {
|
||||
func serverMessage(from data: Data) -> String? {
|
||||
if let envelope = try? JSONDecoder().decode(ApiEnvelope<EmptyResult>.self, from: data) {
|
||||
return envelope.message
|
||||
}
|
||||
@@ -172,7 +289,29 @@ final class ApiClient {
|
||||
return String(data: data, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
|
||||
#if canImport(LCEssentials) && os(iOS)
|
||||
func serverMessage(from error: NSError) -> String? {
|
||||
if let reason = error.userInfo[NSLocalizedFailureReasonErrorKey] as? String,
|
||||
!reason.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
if let data = reason.data(using: .utf8),
|
||||
let parsed = serverMessage(from: data),
|
||||
!parsed.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
return parsed
|
||||
}
|
||||
return reason
|
||||
}
|
||||
|
||||
if let description = error.userInfo[NSLocalizedDescriptionKey] as? String,
|
||||
!description.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
||||
!description.lowercased().contains("nsurlerrordomain") {
|
||||
return description
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
#endif
|
||||
|
||||
func buildURL(path: String, query: [URLQueryItem]) throws -> URL {
|
||||
guard var components = URLComponents(url: ApiConfig.baseURL, resolvingAgainstBaseURL: false) else {
|
||||
throw NetworkError.invalidURL
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user