Initial commit - cleaned for CV

This commit is contained in:
Renato97
2026-03-31 01:28:25 -03:00
commit 2e7bb89d77
6413 changed files with 1069318 additions and 0 deletions

View File

@@ -0,0 +1,434 @@
import Foundation
/// Configuración centralizada de la API del backend VPS.
///
/// `APIConfig` proporciona todos los endpoints y parámetros de configuración
/// necesarios para comunicarse con el backend VPS que gestiona el almacenamiento
/// y serving de capítulos de manga.
///
/// # Example
/// ```swift
/// let baseURL = APIConfig.baseURL
/// let downloadEndpoint = APIConfig.Endpoints.download(mangaSlug: "one-piece", chapterNumber: 1)
/// print(downloadEndpoint) // "https://api.example.com/api/v1/download/one-piece/1"
/// ```
enum APIConfig {
// MARK: - Base Configuration
/// URL base del backend VPS
///
/// Esta URL se usa para construir todos los endpoints de la API.
/// Configurar según el entorno (desarrollo, staging, producción).
///
/// # Configuración Actual
/// - Producción: `https://gitea.cbcren.online`
/// - Puerto: `3001` (se añade automáticamente)
///
/// # Notas Importantes
/// - Incluir el protocolo (`https://` o `http://`)
/// - NO incluir el número de puerto aquí (usar la propiedad `port`)
/// - NO incluir slash al final
/// - Asegurarse de que el servidor sea accesible desde el dispositivo iOS
///
/// # Ejemplos
/// - `https://manga.cbcren.online` (VPS de producción)
/// - `http://192.168.1.100` (desarrollo local)
/// - `http://localhost` (simulador con servidor local)
static let serverURL = "https://manga.cbcren.online"
/// Puerto donde corre el backend API
///
/// # Valor por Defecto
/// - `nil` - Usa el puerto estándar HTTPS (443)
///
/// # Notas
/// - Con HTTPS, se usa el puerto estándar 443
/// - Solo especificar un puerto si es diferente al estándar
static let port: Int? = nil
/// URL base completa para requests a la API
///
/// Construye automáticamente la URL base combinando la URL del servidor y el puerto.
/// Esta es la propiedad recomendada para usar al hacer requests a la API.
///
/// # Ejemplo
/// ```swift
/// let endpoint = "/api/v1/manga"
/// let url = URL(string: APIConfig.baseURL + endpoint)
/// ```
static var baseURL: String {
// Si no hay puerto específico o es el puerto estándar HTTPS, usar solo la URL
if port == nil || port == 443 {
return serverURL
}
return "\(serverURL):\(port!)"
}
/// Versión de la API
static var apiVersion: String {
return "v1"
}
/// Path base de la API
static var basePath: String {
return "\(baseURL)/api/\(apiVersion)"
}
/// Timeout por defecto para requests (en segundos)
static var defaultTimeout: TimeInterval {
return 30.0
}
/// Timeout para requests de descarga (en segundos)
static var downloadTimeout: TimeInterval {
return 300.0 // 5 minutos
}
// MARK: - HTTP Headers
/// Headers HTTP comunes para todas las requests
static var commonHeaders: [String: String] {
return [
"Content-Type": "application/json",
"Accept": "application/json"
]
}
/// Header de autenticación (si se requiere API key o token)
///
/// - Parameter token: Token de autenticación (API key, JWT, etc.)
/// - Returns: Dictionary con el header de autorización
static func authHeader(token: String) -> [String: String] {
return [
"Authorization": "Bearer \(token)"
]
}
// MARK: - Retry Configuration
/// Número máximo de intentos de retry para requests fallidas
///
/// # Valor por Defecto
/// - `3` intentos de retry
///
/// # Comportamiento
/// - Un valor de `0` significa sin reintentos
/// - Los reintentos usan backoff exponencial
/// - Solo se reintentan errores recuperables (fallos de red, timeouts, etc.)
/// - Errores de cliente (4xx) típicamente no se reintentan
static let maxRetries: Int = 3
/// Delay base entre intentos de retry en segundos
///
/// # Valor por Defecto
/// - `1.0` segundo
///
/// # Fórmula
/// El delay real usa backoff exponencial:
/// ```
/// delay = baseRetryDelay * (2 ^ numeroDeIntento)
/// ```
/// - Intento 1: 1 segundo de delay
/// - Intento 2: 2 segundos de delay
/// - Intento 3: 4 segundos de delay
static let baseRetryDelay: TimeInterval = 1.0
// MARK: - Cache Configuration
/// Número máximo de respuestas de API a cachear en memoria
///
/// # Valor por Defecto
/// - `100` respuestas cacheadas
///
/// # Notas
/// - Cachear ayuda a reducir requests de red y mejorar performance
/// - La caché se limpia automáticamente cuando se detecta presión de memoria
/// - Valores más grandes pueden aumentar el uso de memoria
static let cacheMaxMemoryUsage = 100
/// Tiempo de expiración de caché para respuestas de API en segundos
///
/// # Valor por Defecto
/// - `300.0` segundos (5 minutos)
///
/// # Uso
/// - Datos cacheados más viejos que esto se refrescarán del servidor
/// - Configurar en `0` para deshabilitar caché
/// - Aumentar para datos que cambian infrecuentemente
static let cacheExpiryTime: TimeInterval = 300.0
// MARK: - Logging Configuration
/// Habilitar logging de requests para debugging
///
/// # Valor por Defecto
/// - `false` (deshabilitado en producción)
///
/// # Comportamiento
/// - Cuando es `true`, todas las requests y respuestas se loguean a consola
/// - Útil para desarrollo y debugging
/// - Debe ser `false` en builds de producción por seguridad
///
/// # Recomendación
/// Usar configuraciones de build para habilitar solo en debug:
/// ```swift
/// #if DEBUG
/// static let loggingEnabled = true
/// #else
/// static let loggingEnabled = false
/// #endif
/// ```
static let loggingEnabled = false
// MARK: - Helper Methods
/// Construye una URL completa para un endpoint dado
///
/// - Parameter endpoint: El path del endpoint de la API (ej: "/manga/popular")
/// - Returns: Una URL completa combinando la base URL y el endpoint
///
/// # Ejemplo
/// ```swift
/// let url = APIConfig.url(for: "/manga/popular")
/// // Retorna: "https://gitea.cbcren.online:3001/api/v1/manga/popular"
/// ```
static func url(for endpoint: String) -> String {
// Remover slash inicial si está presente para evitar dobles slashes
let cleanEndpoint = endpoint.hasPrefix("/") ? String(endpoint.dropFirst()) : endpoint
// Añadir prefix de API si no está ya incluido
if cleanEndpoint.hasPrefix("api/") {
return baseURL + "/" + cleanEndpoint
} else {
return baseURL + "/" + cleanEndpoint
}
}
/// Crea un objeto URL para un endpoint dado
///
/// - Parameter endpoint: El path del endpoint de la API
/// - Returns: Un objeto URL, o nil si el string es inválido
///
/// # Ejemplo
/// ```swift
/// if let url = APIConfig.urlObject(for: "/manga/popular") {
/// var request = URLRequest(url: url)
/// // Hacer request...
/// }
/// ```
static func urlObject(for endpoint: String) -> URL? {
return URL(string: url(for: endpoint))
}
/// Retorna el timeout a usar para un tipo específico de request
///
/// - Parameter isResourceRequest: Si esta es una request intensiva de recursos (ej: descargar imágenes)
/// - Returns: El valor de timeout apropiado
///
/// # Ejemplo
/// ```swift
/// let timeout = APIConfig.timeoutFor(isResourceRequest: true)
/// // Retorna: 300.0 (downloadTimeout)
/// ```
static func timeoutFor(isResourceRequest: Bool = false) -> TimeInterval {
return isResourceRequest ? downloadTimeout : defaultTimeout
}
// MARK: - Validation
/// Valida que la configuración actual esté correctamente configurada
///
/// - Returns: `true` si la configuración parece válida, `false` en caso contrario
///
/// # Verificaciones Realizadas
/// - URL del servidor no está vacía
/// - Puerto está en rango válido (1-65535)
/// - Valores de timeout son positivos
/// - Cantidad de reintentos es no-negativa
///
/// # Uso
/// Llamar durante el inicio de la app para asegurar configuración válida:
/// ```swift
/// assert(APIConfig.isValid, "Configuración de API inválida")
/// ```
static var isValid: Bool {
// Verificar URL del servidor
guard !serverURL.isEmpty else { return false }
// Verificar rango de puerto
guard (1...65535).contains(port) else { return false }
// Verificar timeouts
guard defaultTimeout > 0 && downloadTimeout > 0 else { return false }
// Verificar cantidad de reintentos
guard maxRetries >= 0 else { return false }
return true
}
// MARK: - Endpoints
/// Estructura que contiene todos los endpoints de la API
enum Endpoints {
/// Endpoint para solicitar la descarga de un capítulo al VPS
///
/// El backend iniciará el proceso de descarga de las imágenes
/// y las almacenará en el VPS.
///
/// - Parameters:
/// - mangaSlug: Slug del manga a descargar
/// - chapterNumber: Número del capítulo
/// - Returns: URL completa del endpoint
static func download(mangaSlug: String, chapterNumber: Int) -> String {
return "\(basePath)/download/\(mangaSlug)/\(chapterNumber)"
}
/// Endpoint para verificar si un capítulo está descargado en el VPS
///
/// - Parameters:
/// - mangaSlug: Slug del manga
/// - chapterNumber: Número del capítulo
/// - Returns: URL completa del endpoint
static func checkDownloaded(mangaSlug: String, chapterNumber: Int) -> String {
return "\(basePath)/chapters/\(mangaSlug)/\(chapterNumber)"
}
/// Endpoint para listar todos los capítulos descargados de un manga
///
/// - Parameter mangaSlug: Slug del manga
/// - Returns: URL completa del endpoint
static func listChapters(mangaSlug: String) -> String {
return "\(basePath)/chapters/\(mangaSlug)"
}
/// Endpoint para obtener la URL de una imagen específica de un capítulo
///
/// - Parameters:
/// - mangaSlug: Slug del manga
/// - chapterNumber: Número del capítulo
/// - pageIndex: Índice de la página (0-based)
/// - Returns: URL completa del endpoint
static func getImage(mangaSlug: String, chapterNumber: Int, pageIndex: Int) -> String {
return "\(basePath)/images/\(mangaSlug)/\(chapterNumber)/\(pageIndex)"
}
/// Endpoint para eliminar un capítulo del VPS
///
/// - Parameters:
/// - mangaSlug: Slug del manga
/// - chapterNumber: Número del capítulo
/// - Returns: URL completa del endpoint
static func deleteChapter(mangaSlug: String, chapterNumber: Int) -> String {
return "\(basePath)/chapters/\(mangaSlug)/\(chapterNumber)"
}
/// Endpoint para obtener estadísticas de almacenamiento del VPS
///
/// - Returns: URL completa del endpoint
static func storageStats() -> String {
return "\(basePath)/storage/stats"
}
/// Endpoint para hacer ping al servidor (health check)
///
/// - Returns: URL completa del endpoint
static func health() -> String {
return "\(basePath)/health"
}
}
// MARK: - Error Codes
/// Códigos de error específicos de la API
enum ErrorCodes {
static let chapterNotFound = 40401
static let chapterAlreadyDownloaded = 40901
static let storageLimitExceeded = 50701
static let invalidImageFormat = 42201
static let downloadFailed = 50001
}
// MARK: - Environment Configuration
/// Configuración para entorno de desarrollo
///
/// # Uso
/// Para usar configuración de desarrollo, modificar en builds de desarrollo:
/// ```swift
/// #if DEBUG
/// static let serverURL = "http://192.168.1.100"
/// #else
/// static let serverURL = "https://gitea.cbcren.online"
/// #endif
/// ```
static var development: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
return (
serverURL: "http://192.168.1.100",
port: 3001,
timeout: 60.0,
logging: true
)
}
/// Configuración para entorno de staging
static var staging: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
return (
serverURL: "https://staging.cbcren.online",
port: 3001,
timeout: 30.0,
logging: true
)
}
/// Configuración para entorno de producción
static var production: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
return (
serverURL: "https://gitea.cbcren.online",
port: 3001,
timeout: 30.0,
logging: false
)
}
}
// MARK: - Debug Support
#if DEBUG
extension APIConfig {
/// Configuración de test para unit testing
///
/// # Uso
/// Usar esta configuración en unit tests para evitar hacer llamadas reales a la API:
/// ```swift
/// func testAPICall() {
/// let testConfig = APIConfig.testing
/// // Usar URL de servidor mock
/// }
/// ```
static var testing: (serverURL: String, port: Int, timeout: TimeInterval, logging: Bool) {
return (
serverURL: "http://localhost:3001",
port: 3001,
timeout: 5.0,
logging: true
)
}
/// Imprime la configuración actual a consola (solo debug)
static func printConfiguration() {
print("=== API Configuration ===")
print("Server URL: \(serverURL)")
print("Port: \(port)")
print("Base URL: \(baseURL)")
print("API Version: \(apiVersion)")
print("Default Timeout: \(defaultTimeout)s")
print("Download Timeout: \(downloadTimeout)s")
print("Max Retries: \(maxRetries)")
print("Logging Enabled: \(loggingEnabled)")
print("Cache Enabled: \(cacheExpiryTime > 0)")
print("=========================")
}
}
#endif

View File

@@ -0,0 +1,290 @@
import Foundation
/// Ejemplos de uso de APIConfig
///
/// Este archivo demuestra cómo utilizar la configuración de la API
/// en diferentes escenarios de la aplicación.
class APIConfigExample {
/// Ejemplo 1: Configurar URLSession con timeouts de APIConfig
func configureURLSession() -> URLSession {
let configuration = URLSessionConfiguration.default
configuration.timeoutIntervalForRequest = APIConfig.defaultTimeout
configuration.timeoutIntervalForResource = APIConfig.downloadTimeout
return URLSession(configuration: configuration)
}
/// Ejemplo 2: Construir una URL completa para un endpoint
func buildEndpointURL() {
// Método 1: Usar la función helper
let url1 = APIConfig.url(for: "manga/popular")
print("URL completa: \(url1)")
// Método 2: Usar urlObject para obtener un objeto URL
if let url2 = APIConfig.urlObject(for: "manga/popular") {
print("URL object: \(url2)")
}
// Método 3: Usar directamente baseURL
let url3 = "\(APIConfig.basePath)/manga/popular"
print("URL manual: \(url3)")
}
/// Ejemplo 3: Usar los endpoints predefinidos
func usePredefinedEndpoints() {
// Endpoint de descarga
let downloadURL = APIConfig.Endpoints.download(
mangaSlug: "one-piece",
chapterNumber: 1089
)
print("Download endpoint: \(downloadURL)")
// Endpoint de verificación
let checkURL = APIConfig.Endpoints.checkDownloaded(
mangaSlug: "one-piece",
chapterNumber: 1089
)
print("Check endpoint: \(checkURL)")
// Endpoint de imagen
let imageURL = APIConfig.Endpoints.getImage(
mangaSlug: "one-piece",
chapterNumber: 1089,
pageIndex: 0
)
print("Image endpoint: \(imageURL)")
// Endpoint de health check
let healthURL = APIConfig.Endpoints.health()
print("Health endpoint: \(healthURL)")
// Endpoint de estadísticas de almacenamiento
let statsURL = APIConfig.Endpoints.storageStats()
print("Storage stats endpoint: \(statsURL)")
}
/// Ejemplo 4: Crear una URLRequest con headers comunes
func createRequest() -> URLRequest? {
let endpoint = "manga/popular"
guard let url = APIConfig.urlObject(for: endpoint) else {
return nil
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
// Añadir headers comunes
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Si se requiere autenticación
// let token = "your-auth-token"
// let authHeaders = APIConfig.authHeader(token: token)
// for (key, value) in authHeaders {
// request.setValue(value, forHTTPHeaderField: key)
// }
return request
}
/// Ejemplo 5: Validar la configuración al iniciar la app
func validateConfiguration() {
#if DEBUG
// Imprimir configuración en debug
APIConfig.printConfiguration()
#endif
// Validar que la configuración sea correcta
guard APIConfig.isValid else {
print("ERROR: Configuración de API inválida")
return
}
print("Configuración válida: \(APIConfig.baseURL)")
}
/// Ejemplo 6: Hacer una request simple
func makeSimpleRequest() async throws {
let endpoint = "manga/popular"
guard let url = APIConfig.urlObject(for: endpoint) else {
print("URL inválida")
return
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.timeoutFor(isResourceRequest: false)
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
print("Status code: \(httpResponse.statusCode)")
}
// Procesar data...
print("Recibidos \(data.count) bytes")
}
/// Ejemplo 7: Usar timeouts apropiados según el tipo de request
func demonstrateTimeouts() {
// Request normal (usar defaultTimeout)
let normalTimeout = APIConfig.timeoutFor(isResourceRequest: false)
print("Normal timeout: \(normalTimeout)s") // 30.0s
// Request de descarga de imagen (usar downloadTimeout)
let resourceTimeout = APIConfig.timeoutFor(isResourceRequest: true)
print("Resource timeout: \(resourceTimeout)s") // 300.0s
}
/// Ejemplo 8: Cambiar configuración según el entorno
func configureForEnvironment() {
#if DEBUG
// En desarrollo, usar configuración local
print("Modo desarrollo")
// Nota: Para cambiar realmente la configuración, modificar las propiedades
// estáticas en APIConfig usando compilación condicional
#else
// En producción, usar configuración de producción
print("Modo producción")
#endif
}
/// Ejemplo 9: Manejar errores específicos de la API
func handleAPIError(errorCode: Int) {
switch errorCode {
case APIConfig.ErrorCodes.chapterNotFound:
print("Capítulo no encontrado")
case APIConfig.ErrorCodes.chapterAlreadyDownloaded:
print("Capítulo ya descargado")
case APIConfig.ErrorCodes.storageLimitExceeded:
print("Límite de almacenamiento excedido")
case APIConfig.ErrorCodes.invalidImageFormat:
print("Formato de imagen inválido")
case APIConfig.ErrorCodes.downloadFailed:
print("Descarga fallida")
default:
print("Error desconocido: \(errorCode)")
}
}
/// Ejemplo 10: Implementar retry con backoff exponencial
func fetchWithRetry(endpoint: String, retryCount: Int = 0) async throws -> Data {
guard let url = APIConfig.urlObject(for: endpoint) else {
throw URLError(.badURL)
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
do {
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse,
httpResponse.statusCode == 200 {
return data
} else {
throw URLError(.badServerResponse)
}
} catch {
// Verificar si debemos reintentar
if retryCount < APIConfig.maxRetries {
// Calcular delay con backoff exponencial
let delay = APIConfig.baseRetryDelay * pow(2.0, Double(retryCount))
print("Retry \(retryCount + 1) después de \(delay)s")
try await Task.sleep(nanoseconds: UInt64(delay * 1_000_000_000))
return try await fetchWithRetry(endpoint: endpoint, retryCount: retryCount + 1)
} else {
throw error
}
}
}
}
// MARK: - Usage Examples
// Ejemplo de uso en una ViewModel o Service:
class MangaServiceExample {
func fetchPopularManga() async throws {
// Usar endpoint predefinido
let endpoint = "manga/popular"
guard let url = APIConfig.urlObject(for: endpoint) else {
return
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
// Añadir headers
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Hacer request
let (data, _) = try await URLSession.shared.data(for: request)
// Parsear respuesta...
print("Datos recibidos: \(data.count) bytes")
}
func downloadChapter(mangaSlug: String, chapterNumber: Int) async throws {
// Usar endpoint predefinido
let endpoint = APIConfig.Endpoints.download(
mangaSlug: mangaSlug,
chapterNumber: chapterNumber
)
guard let url = URL(string: endpoint) else {
return
}
var request = URLRequest(url: url)
// Usar timeout más largo para descargas
request.timeoutInterval = APIConfig.downloadTimeout
for (key, value) in APIConfig.commonHeaders {
request.setValue(value, forHTTPHeaderField: key)
}
// Hacer request
let (data, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
print("Status: \(httpResponse.statusCode)")
// Manejar errores específicos
if httpResponse.statusCode != 200 {
// Aquí podrías usar APIConfig.ErrorCodes si el backend
// retorna códigos de error personalizados
throw URLError(.badServerResponse)
}
}
print("Descarga completada: \(data.count) bytes")
}
func checkServerHealth() async throws {
// Usar endpoint de health check
let endpoint = APIConfig.Endpoints.health()
guard let url = URL(string: endpoint) else {
return
}
var request = URLRequest(url: url)
request.timeoutInterval = APIConfig.defaultTimeout
let (_, response) = try await URLSession.shared.data(for: request)
if let httpResponse = response as? HTTPURLResponse {
print("Server health status: \(httpResponse.statusCode)")
}
}
}