Skip to content

Latest commit

 

History

History
438 lines (320 loc) · 13.6 KB

File metadata and controls

438 lines (320 loc) · 13.6 KB

GentleNetworking

Uma biblioteca de networking leve, pronta para Swift 6, projetada para apps iOS modernos usando async/await, arquitetura limpa e abstrações testáveis.

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

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


✨ Funcionalidades

  • ✅ API nativa com async/await
  • ✅ Camada de networking baseada em protocolos, totalmente mockável
  • ✅ Decodificação tipada de request / response
  • ✅ Compatível com Swift 6 + Swift Concurrency
  • ✅ Projetada para MVVM / Clean Architecture
  • ✅ Zero dependências de terceiros
  • ✅ Transports com respostas predefinidas para testes
  • ✅ Retry com backoff exponencial e jitter
  • ✅ Renovação de tokens e re-autenticação transparente
  • ✅ GET condicional via ETag / If-None-Match / 304

💬 Participe da discussão. Feedback e perguntas são bem-vindos


App de Demonstração

Uma app de demonstração em SwiftUI executável está incluída neste repositório usando uma referência local ao pacote.

Como Executar

  1. Clone o repositório:
    git clone https://github.com/gentle-giraffe-apps/GentleNetworking.git
  2. Abra o projeto de demonstração:
    Demo/GentleNetworkingDemo/GentleNetworkingDemo.xcodeproj
    
  3. Selecione um simulador com iOS 17+.
  4. Compile e execute (⌘R).

O projeto vem pré-configurado com uma referência local ao pacote Swift GentleNetworking e deve funcionar sem configuração adicional.


📦 Instalação (Swift Package Manager)

Via Xcode

  1. Abra seu projeto no Xcode
  2. Vá em File → Add Packages...
  3. Insira a URL do repositório: https://github.com/gentle-giraffe-apps/GentleNetworking.git
  4. Escolha uma regra de versão (ou main durante o desenvolvimento)
  5. Adicione o produto GentleNetworking ao seu target

Via Package.swift

Adicione a dependência ao seu Package.swift:

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

Em seguida, adicione "GentleNetworking" ao target que precisar:

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

Qualidade e Ferramentas

Este projeto aplica controles de qualidade via CI e análise estática:

  • CI: Todos os commits em main devem passar nas verificações do GitHub Actions
  • Análise estática: DeepSource é executado em cada commit em main. O badge indica o número atual de issues de análise estática pendentes.
  • Cobertura de testes: Codecov reporta a cobertura de linhas para a branch main

Snapshot do Codecov
Gráfico de cobertura do Codecov

Essas verificações são projetadas para manter o sistema seguro à medida que evolui.


Arquitetura

GentleNetworking é centrado em um único HTTPNetworkService baseado em protocolos que coordena as requisições usando abstrações injetadas de endpoint, ambiente e autenticação.

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 -->|injetado| 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 uma API e 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. Criar um Network Service

let networkService = HTTPNetworkService()

3. Autenticar se Necessário

SystemKeyChainAuthService é a implementação integrada do AuthServiceProtocol. Ele armazena um token Bearer no keychain do sistema e o anexa automaticamente às requisições de endpoints onde requiresAuth é 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 um Modelo

Use request para decodificar um único objeto da resposta:

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 um Array de Modelos

Use requestModels para decodificar um array de objetos da resposta:

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

🧪 Testes

GentleNetworking fornece uma abstração na camada de transporte para facilitar o mocking em testes.

CannedResponseTransport

Retorna uma resposta fixa para qualquer requisição:

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

let networkService = HTTPNetworkService(transport: transport)

CannedRoutesTransport

Associa requisições por método e padrão de rota para cenários de teste mais 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)

🔒 Segurança

GentleNetworking utiliza o App Transport Security (ATS) da Apple para proteção da camada de transporte — TLS 1.2+, validação de certificados, forward secrecy — tudo aplicado pelo sistema operacional e habilitado por padrão.

Pinning de Certificado SSL

Para apps com requisitos de segurança elevados, use o PinningTransport integrado com pinning de chave pública ou de certificado:

import CryptoKit

// Pinning de chave pública (recomendado — sobrevive a renovações de certificado)
let service = HTTPNetworkService(
    transport: PinningTransport(
        pinnedDomains: [
            "api.example.com": PublicKeyPinningEvaluator(
                pinnedKeyHashes: [primaryKeyHash, backupKeyHash]
            )
        ]
    )
)

// Pinning de certificado (mais simples, falha a cada renovação)
let service = HTTPNetworkService(
    transport: PinningTransport(
        pinnedDomains: [
            "api.example.com": CertificatePinningEvaluator(
                pinnedCertificates: [certDERData]
            )
        ]
    )
)

Domínios sem pinning utilizam a validação padrão do ATS. Implemente ServerTrustEvaluator para lógica de confiança personalizada.

Consulte SECURITY.md para o guia completo incluindo melhores práticas, avaliadores personalizados e abordagens alternativas.


🔄 Retry e Re-Autenticação

GentleNetworking fornece wrappers de transporte combináveis para lógica de retry e renovação automática de tokens. Por serem transports, eles se empilham entre si e com PinningTransport.

RetryTransport

Repete requisições que falharam com backoff exponencial e jitter configurável. Por padrão, repete em 429, 500, 503 e erros de rede — nunca em 401 ou outros erros 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 respostas HTTP 401, renova o token via closure fornecido, re-autoriza a requisição original e a repete uma vez. 401s concorrentes são serializados para que apenas uma renovação ocorra.

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 o re-download de recursos caros e inalterados. No primeiro GET, o cabeçalho ETag do servidor e o corpo da resposta são armazenados em cache. GETs subsequentes enviam If-None-Match; se o servidor responder 304 Not Modified, o corpo em cache é retornado sem transferir o payload novamente.

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

Injete um ETagStoreProtocol personalizado para persistência em disco ou banco de dados:

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

Ordem de Empilhamento

Coloque ReauthTransport no exterior, RetryTransport no meio, e ETagTransport no interior:

ReauthTransport          ← captura 401 após esgotar os retries
  └─ RetryTransport      ← repete 429/500/503 com backoff + jitter
       └─ ETagTransport  ← GET condicional via ETag / 304
            └─ URLSessionTransport (ou PinningTransport)

RetryTransport já ignora 401 (defaultShouldRetry retorna false), então passa falhas de autenticação diretamente para ReauthTransport sem desperdiçar retries. ETagTransport fica dentro do retry para que requisições refeitas também se beneficiem do cache.


🧭 Filosofia de Design

GentleNetworking é construído em torno de:

  • ✅ Previsibilidade acima de mágica
  • ✅ Design baseado em protocolos
  • ✅ Injeção de dependências explícita
  • ✅ Concorrência moderna do Swift
  • ✅ Testabilidade por padrão
  • ✅ Superfície de API pequena com garantias sólidas

É intencionalmente mínimo e evita sobre-abstrair ou ocultar o comportamento de networking.


🤖 Nota sobre Ferramentas

Partes da redação e do refinamento editorial neste repositório foram acelerados usando modelos de linguagem grandes (incluindo ChatGPT, Claude e Gemini) sob design humano direto, validação e aprovação final. Todas as decisões técnicas, código e conclusões arquiteturais são de autoria e verificação do mantenedor do repositório.


🔐 Licença

Licença MIT Livre para uso pessoal e comercial.


👤 Autor

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

Visitors