Not has address

This commit is contained in:
Daniel Arantes Loverde
2026-02-07 17:16:45 -03:00
parent 92c2a67736
commit 54686397b9
28 changed files with 2115 additions and 498 deletions

View File

@@ -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
}