Skip to content

Latest commit

 

History

History
438 lines (320 loc) · 13.8 KB

File metadata and controls

438 lines (320 loc) · 13.8 KB

GentleNetworking

Una biblioteca de networking ligera, lista para Swift 6, diseñada para apps iOS modernas con async/await, arquitectura limpia y abstracciones testeables.

🌍 Idioma · English · Español · Português (Brasil) · 日本語 · 简体中文 · 한국어 · Русский

Build Coverage Swift SPM Compatible Platform Commit activity Last commit DeepSource Static Analysis DeepSource


✨ Características

  • ✅ API nativa con async/await
  • ✅ Capa de networking basada en protocolos, completamente mockeable
  • ✅ Decodificación tipada de request / response
  • ✅ Compatible con Swift 6 + Swift Concurrency
  • ✅ Diseñada para MVVM / Clean Architecture
  • ✅ Sin dependencias de terceros
  • ✅ Transports con respuestas predefinidas para testing
  • ✅ Reintentos con backoff exponencial y jitter
  • ✅ Renovación de tokens y re-autenticación transparente
  • ✅ GET condicional vía ETag / If-None-Match / 304

💬 Únete a la discusión. Comentarios y preguntas son bienvenidos


App de Demostración

Se incluye una app de demostración en SwiftUI en este repositorio usando una referencia local al paquete.

Cómo Ejecutar

  1. Clona el repositorio:
    git clone https://github.com/gentle-giraffe-apps/GentleNetworking.git
  2. Abre el proyecto de demostración:
    Demo/GentleNetworkingDemo/GentleNetworkingDemo.xcodeproj
  3. Selecciona un simulador con iOS 17+.
  4. Compila y ejecuta (⌘R).

El proyecto viene preconfigurado con una referencia local al paquete Swift GentleNetworking y debería funcionar sin configuración adicional.


📦 Instalación (Swift Package Manager)

Vía Xcode

  1. Abre tu proyecto en Xcode
  2. Ve a File → Add Packages...
  3. Ingresa la URL del repositorio: https://github.com/gentle-giraffe-apps/GentleNetworking.git
  4. Elige una regla de versión (o main durante el desarrollo)
  5. Agrega el producto GentleNetworking a tu target

Vía Package.swift

Agrega la dependencia a tu Package.swift:

dependencies: [
    .package(url: "https://github.com/gentle-giraffe-apps/GentleNetworking.git", from: "1.0.0")
]

Luego agrega "GentleNetworking" al target que lo necesite:

.target(
    name: "YourApp",
    dependencies: ["GentleNetworking"]
)

Calidad y Herramientas

Este proyecto aplica controles de calidad mediante CI y análisis estático:

  • CI: Todos los commits a main deben pasar las verificaciones de GitHub Actions
  • Análisis estático: DeepSource se ejecuta en cada commit a main. El badge indica el número actual de issues de análisis estático pendientes.
  • Cobertura de tests: Codecov reporta la cobertura de líneas para la rama main

Snapshot de Codecov
Gráfico de cobertura de Codecov

Estas verificaciones están diseñadas para mantener el sistema seguro a medida que evoluciona.


Arquitectura

GentleNetworking está centrado en un único HTTPNetworkService basado en protocolos que coordina las peticiones usando abstracciones inyectadas de endpoint, entorno y autenticación.

flowchart TB
    HTTP["HTTPNetworkService<br/><br/>- request(...)"]

    Endpoint["EndpointProtocol<br/><br/><br/>"]
    Env["APIEnvironmentProtocol<br/><br/><br/>"]
    Auth["AuthServiceProtocol<br/><br/><br/>"]

    HTTP --> Endpoint
    HTTP --> Env
    HTTP -->|inyectado| Auth
Loading

Endpoint

flowchart TB
    APIEndpoint["APIEndpoint enum<br/><br/>case endpoint1<br/>…<br/>endpointN"]

    EndpointProtocol["EndpointProtocol<br/><br/>- path<br/>- method<br/>- query<br/>- body<br/>- requiresAuth"]

    APIEndpoint -->|conforma a| EndpointProtocol
Loading

🚀 Uso Básico

1. Definir una API y Endpoints

import GentleNetworking

let apiEnvironment = DefaultAPIEnvironment(
    baseURL: URL(string: "https://api.company.com")
)

nonisolated enum APIEndpoint: EndpointProtocol {
    case signIn(username: String, password: String)
    case model(id: Int)
    case models

    var path: String {
        switch self {
        case .signIn: "/api/signIn"
        case .model(let id): "/api/model/\(id)"
        case .models: "/api/models"
        }
    }

    var method: HTTPMethod {
        switch self {
        case .signIn: .post
        case .model, .models: .get
        }
    }

    var query: [URLQueryItem]? {
        switch self {
        case .signIn, .model, .models: nil
        }
    }

    var body: [String: EndpointAnyEncodable]? {
        switch self {
        case .signIn(let username, let password): [
            "username": EndpointAnyEncodable(username),
            "password": EndpointAnyEncodable(password)
        ]
        case .model, .models: nil
        }
    }

    var requiresAuth: Bool {
        switch self {
        case .model, .models: true
        case .signIn(username: _, password: _): false
        }
    }
}

2. Crear un Network Service

let networkService = HTTPNetworkService()

3. Autenticarse si es Necesario

SystemKeyChainAuthService es la implementación integrada de AuthServiceProtocol. Almacena un token Bearer en el keychain del sistema y lo adjunta automáticamente a las peticiones de endpoints donde requiresAuth es true.

let keyChainAuthService = SystemKeyChainAuthService()

struct AuthTokenModel: Decodable, Sendable {
    let token: String
}

let authTokenModel: AuthTokenModel = try await networkService.request(
    to: .signIn(username: "user", password: "pass"),
    via: apiEnvironment
)

try await keyChainAuthService.saveAccessToken(
    authTokenModel.token
)

4. Solicitar un Modelo

Usa request para decodificar un único objeto de la respuesta:

struct Model: Decodable, Sendable {
    let id: Int
    let property: String
}

let model: Model = try await networkService.request(
    to: .model(id: 123),
    via: apiEnvironment
)

5. Solicitar un Array de Modelos

Usa requestModels para decodificar un array de objetos de la respuesta:

let models: [Model] = try await networkService.requestModels(
    to: .models,
    via: apiEnvironment
)

🧪 Testing

GentleNetworking proporciona una abstracción en la capa de transporte para facilitar el mocking en tests.

CannedResponseTransport

Retorna una respuesta fija para cualquier petición:

let transport = CannedResponseTransport(
    string: #"{"id": 1, "title": "Test"}"#,
    statusCode: 200
)

let networkService = HTTPNetworkService(transport: transport)

CannedRoutesTransport

Asocia peticiones por método y patrón de ruta para escenarios de test más realistas:

let transport = CannedRoutesTransport(routes: [
    CannedRoute(
        pattern: RequestPattern(method: .get, path: "/api/models"),
        response: CannedResponse(string: #"[{"id": 1}]"#)
    ),
    CannedRoute(
        pattern: RequestPattern(method: .post, pathRegex: "^/api/model/\\d+$"),
        response: CannedResponse(string: #"{"success": true}"#)
    )
])

let networkService = HTTPNetworkService(transport: transport)

🔒 Seguridad

GentleNetworking se apoya en App Transport Security (ATS) de Apple para la protección de la capa de transporte — TLS 1.2+, validación de certificados, forward secrecy — todo aplicado por el sistema operativo y habilitado por defecto.

Fijación de Certificados SSL

Para apps con requisitos de seguridad elevados, usa el PinningTransport integrado con fijación de clave pública o de certificado:

import CryptoKit

// Fijación de clave pública (recomendado — sobrevive a renovaciones de certificado)
let service = HTTPNetworkService(
    transport: PinningTransport(
        pinnedDomains: [
            "api.example.com": PublicKeyPinningEvaluator(
                pinnedKeyHashes: [primaryKeyHash, backupKeyHash]
            )
        ]
    )
)

// Fijación de certificado (más simple, falla con cada renovación)
let service = HTTPNetworkService(
    transport: PinningTransport(
        pinnedDomains: [
            "api.example.com": CertificatePinningEvaluator(
                pinnedCertificates: [certDERData]
            )
        ]
    )
)

Los dominios sin fijación utilizan la validación estándar de ATS. Implementa ServerTrustEvaluator para lógica de confianza personalizada.

Consulta SECURITY.md para la guía completa incluyendo mejores prácticas, evaluadores personalizados y enfoques alternativos.


🔄 Reintentos y Re-Autenticación

GentleNetworking proporciona wrappers de transporte componibles para la lógica de reintentos y la renovación automática de tokens. Al ser transports, se apilan entre sí y con PinningTransport.

RetryTransport

Reintenta peticiones fallidas con backoff exponencial y jitter configurable. Por defecto reintenta en 429, 500, 503 y errores de red — nunca en 401 u otros errores de cliente.

let service = HTTPNetworkService(
    transport: RetryTransport(
        inner: URLSessionTransport(session: .shared),
        policy: RetryPolicy(
            maxRetries: 3,
            baseDelay: 0.5,
            maxDelay: 30.0,
            jitter: .full       // .full | .equal | .decorrelated
        )
    )
)

ReauthTransport

Intercepta respuestas HTTP 401, renueva el token mediante un closure proporcionado, re-autoriza la petición original y la reintenta una vez. Los 401 concurrentes se serializan para que solo ocurra una renovación.

let service = HTTPNetworkService(
    transport: ReauthTransport(
        inner: RetryTransport(),
        authService: keyChainAuthService,
        refreshToken: {
            // 1. Call your refresh endpoint to obtain a new access credential
            // 2. Save the new credential via authService so future requests use it
        }
    )
)

ETagTransport

Evita volver a descargar recursos costosos que no han cambiado. En el primer GET, el encabezado ETag del servidor y el cuerpo de la respuesta se almacenan en caché. Los GETs posteriores envían If-None-Match; si el servidor responde 304 Not Modified, se devuelve el cuerpo en caché sin transferir la carga útil nuevamente.

let service = HTTPNetworkService(
    transport: ETagTransport(
        inner: URLSessionTransport(session: .shared)
    )
)

Inyecta un ETagStoreProtocol personalizado para persistencia respaldada por disco o base de datos:

let service = HTTPNetworkService(
    transport: ETagTransport(
        inner: URLSessionTransport(session: .shared),
        store: MyDiskETagStore()
    )
)

Orden de Apilamiento

Coloca ReauthTransport en el exterior, RetryTransport en el medio, y ETagTransport en el interior:

ReauthTransportcaptura 401 después de agotar los reintentos
  └─ RetryTransportreintenta 429/500/503 con backoff + jitter
       └─ ETagTransportGET condicional vía ETag / 304
            └─ URLSessionTransport (o PinningTransport)

RetryTransport ya omite el 401 (defaultShouldRetry devuelve false), por lo que pasa los fallos de autenticación directamente a ReauthTransport sin desperdiciar reintentos. ETagTransport se ubica dentro de retry para que las peticiones reintentadas también se beneficien de la caché.


🧭 Filosofía de Diseño

GentleNetworking está construido alrededor de:

  • ✅ Predictibilidad sobre magia
  • ✅ Diseño basado en protocolos
  • ✅ Inyección de dependencias explícita
  • ✅ Concurrencia moderna de Swift
  • ✅ Testeabilidad por defecto
  • ✅ Superficie de API pequeña con garantías sólidas

Es intencionalmente mínimo y evita sobre-abstraer u ocultar el comportamiento de networking.


🤖 Nota sobre Herramientas

Partes de la redacción y el refinamiento editorial en este repositorio fueron acelerados usando modelos de lenguaje grandes (incluyendo ChatGPT, Claude y Gemini) bajo diseño humano directo, validación y aprobación final. Todas las decisiones técnicas, código y conclusiones arquitectónicas son autoría y están verificadas por el mantenedor del repositorio.


🔐 Licencia

Licencia MIT Libre para uso personal y comercial.


👤 Autor

Creado por Jonathan Ritchey Gentle Giraffe Apps Senior iOS Engineer --- Swift | SwiftUI | Concurrency

Visitors