Adapter em Rust

O padrao Adapter em Rust: newtype pattern, implementacao de traits estrangeiras, conversoes From/Into, e como adaptar tipos de bibliotecas externas.

Introducao

O Adapter (Adaptador) e um padrao estrutural que permite que interfaces incompativeis trabalhem juntas. Ele atua como um “tradutor” entre dois tipos que nao se comunicam diretamente.

Em Rust, o Adapter e especialmente relevante por causa da regra do orfao (orphan rule): voce nao pode implementar uma trait estrangeira em um tipo estrangeiro. O Newtype Pattern e a solucao idiomatica de Rust para esse problema, criando um wrapper fino que permite implementar qualquer trait em qualquer tipo.

Alem disso, as traits From e Into da biblioteca padrao sao o mecanismo principal de adaptacao em Rust, permitindo conversoes seguras e ergonomicas entre tipos.


Problema

Voce esta integrando uma biblioteca externa de geocodificacao com seu sistema interno. A biblioteca usa seu proprio tipo GeoPoint { latitude: f64, longitude: f64 }, mas seu sistema usa Coordenada { lat: f64, lon: f64 }. Voce nao pode modificar nenhum dos dois tipos, e precisa que eles trabalhem juntos.

// Biblioteca externa (voce nao controla esse codigo)
mod geo_lib {
    pub struct GeoPoint {
        pub latitude: f64,
        pub longitude: f64,
    }

    pub fn calcular_distancia(a: &GeoPoint, b: &GeoPoint) -> f64 {
        // Formula de Haversine simplificada
        let dlat = (b.latitude - a.latitude).to_radians();
        let dlon = (b.longitude - a.longitude).to_radians();
        let a_calc = (dlat / 2.0).sin().powi(2)
            + a.latitude.to_radians().cos()
                * b.latitude.to_radians().cos()
                * (dlon / 2.0).sin().powi(2);
        let c = 2.0 * a_calc.sqrt().asin();
        6371.0 * c // raio da Terra em km
    }
}

// Seu sistema interno (seu codigo)
mod meu_sistema {
    pub struct Coordenada {
        pub lat: f64,
        pub lon: f64,
    }

    pub trait Localizavel {
        fn coordenada(&self) -> Coordenada;
    }
}

// PROBLEMA: GeoPoint e Coordenada sao incompativeis!
// Nao da pra passar Coordenada para calcular_distancia()
// Nao da pra implementar Localizavel para GeoPoint (regra do orfao)

Solucao em Rust

1. Newtype Pattern como Adapter

O Newtype cria um wrapper com custo zero em tempo de execucao:

mod geo_lib {
    #[derive(Debug)]
    pub struct GeoPoint {
        pub latitude: f64,
        pub longitude: f64,
    }

    pub fn calcular_distancia(a: &GeoPoint, b: &GeoPoint) -> f64 {
        let dlat = (b.latitude - a.latitude).to_radians();
        let dlon = (b.longitude - a.longitude).to_radians();
        let a_calc = (dlat / 2.0).sin().powi(2)
            + a.latitude.to_radians().cos()
                * b.latitude.to_radians().cos()
                * (dlon / 2.0).sin().powi(2);
        6371.0 * 2.0 * a_calc.sqrt().asin()
    }
}

/// Coordenada do nosso sistema interno
#[derive(Debug, Clone, Copy)]
pub struct Coordenada {
    pub lat: f64,
    pub lon: f64,
}

/// Trait do nosso sistema
pub trait Localizavel {
    fn coordenada(&self) -> Coordenada;
    fn distancia_ate(&self, outro: &dyn Localizavel) -> f64 {
        let a = self.coordenada();
        let b = outro.coordenada();
        let ga = geo_lib::GeoPoint {
            latitude: a.lat,
            longitude: a.lon,
        };
        let gb = geo_lib::GeoPoint {
            latitude: b.lat,
            longitude: b.lon,
        };
        geo_lib::calcular_distancia(&ga, &gb)
    }
}

/// ADAPTER: Newtype que envolve GeoPoint
/// Custo zero - mesmo layout de memoria que GeoPoint
#[derive(Debug)]
pub struct GeoPointAdapter(pub geo_lib::GeoPoint);

impl Localizavel for GeoPointAdapter {
    fn coordenada(&self) -> Coordenada {
        Coordenada {
            lat: self.0.latitude,
            lon: self.0.longitude,
        }
    }
}

/// Conversao direta: Coordenada -> GeoPoint
impl From<Coordenada> for geo_lib::GeoPoint {
    fn from(coord: Coordenada) -> Self {
        geo_lib::GeoPoint {
            latitude: coord.lat,
            longitude: coord.lon,
        }
    }
}

/// Conversao direta: GeoPoint -> Coordenada
impl From<geo_lib::GeoPoint> for Coordenada {
    fn from(point: geo_lib::GeoPoint) -> Self {
        Coordenada {
            lat: point.latitude,
            lon: point.longitude,
        }
    }
}

/// Exemplo de tipo do nosso sistema
#[derive(Debug)]
pub struct Restaurante {
    pub nome: String,
    pub coordenada: Coordenada,
}

impl Localizavel for Restaurante {
    fn coordenada(&self) -> Coordenada {
        self.coordenada
    }
}

fn main() {
    let restaurante = Restaurante {
        nome: "Fogo de Chao".to_string(),
        coordenada: Coordenada {
            lat: -23.5505,
            lon: -46.6333,
        },
    };

    // Ponto externo adaptado para nosso sistema
    let ponto_externo = GeoPointAdapter(geo_lib::GeoPoint {
        latitude: -22.9068,
        longitude: -43.1729,
    });

    let dist = restaurante.distancia_ate(&ponto_externo);
    println!(
        "Distancia de {} ate o ponto: {:.1} km",
        restaurante.nome, dist
    );

    // Conversao via From/Into
    let coord = Coordenada { lat: -15.7801, lon: -47.9292 };
    let geo_point: geo_lib::GeoPoint = coord.into();
    println!(
        "GeoPoint convertido: ({}, {})",
        geo_point.latitude, geo_point.longitude
    );
}

2. Adapter com From/Into para Conversoes de Tipos

use std::collections::HashMap;

/// Formato de erro da biblioteca externa
#[derive(Debug)]
pub struct ErroExterno {
    pub code: i32,
    pub message: String,
    pub details: Option<String>,
}

/// Nosso formato de erro interno
#[derive(Debug)]
pub struct ErroApp {
    pub tipo: TipoErro,
    pub mensagem: String,
    pub contexto: HashMap<String, String>,
}

#[derive(Debug)]
pub enum TipoErro {
    Validacao,
    NaoEncontrado,
    Autorizacao,
    Interno,
}

/// Adapter: converte erro externo para nosso formato
impl From<ErroExterno> for ErroApp {
    fn from(externo: ErroExterno) -> Self {
        let tipo = match externo.code {
            400..=499 if externo.code == 404 => TipoErro::NaoEncontrado,
            400..=499 if externo.code == 401 || externo.code == 403 => TipoErro::Autorizacao,
            400..=499 => TipoErro::Validacao,
            _ => TipoErro::Interno,
        };

        let mut contexto = HashMap::new();
        contexto.insert("codigo_original".to_string(), externo.code.to_string());
        if let Some(detalhes) = externo.details {
            contexto.insert("detalhes".to_string(), detalhes);
        }

        ErroApp {
            tipo,
            mensagem: externo.message,
            contexto,
        }
    }
}

fn operacao_externa() -> Result<String, ErroExterno> {
    Err(ErroExterno {
        code: 404,
        message: "Recurso nao encontrado".to_string(),
        details: Some("ID: 12345".to_string()),
    })
}

fn main() {
    // O operador ? automaticamente converte ErroExterno -> ErroApp via From
    let resultado: Result<String, ErroApp> =
        operacao_externa().map_err(|e| e.into());

    match resultado {
        Ok(dados) => println!("Sucesso: {}", dados),
        Err(erro) => {
            println!("Erro ({:?}): {}", erro.tipo, erro.mensagem);
            println!("Contexto: {:?}", erro.contexto);
        }
    }
}

Diagrama

NEWTYPE ADAPTER:

    +------------------+         +-------------------+
    |   Seu Codigo     |         |  Biblioteca       |
    |                  |         |  Externa          |
    | trait Localizavel|         |                   |
    |   coordenada()   |         |  GeoPoint         |
    |   distancia()    |         |    latitude: f64  |
    +--------+---------+         |    longitude: f64 |
             |                   +---------+---------+
             |                             |
             |    +---------------------+  |
             +--->| GeoPointAdapter     |<-+
                  | (Newtype wrapper)   |
                  |                     |
                  | impl Localizavel    |
                  |   converte campos   |
                  +---------------------+

    Custo: ZERO em tempo de execucao!
    GeoPointAdapter tem o mesmo layout de memoria que GeoPoint.


FROM/INTO ADAPTER:

    ErroExterno ---> From<ErroExterno> for ErroApp ---> ErroApp
         |                    |                           |
    code: 404         match code {                  tipo: NaoEncontrado
    message: "..."      400..499 => ...             mensagem: "..."
    details: ...      }                             contexto: {...}

Exemplo do Mundo Real

Adaptando diferentes formatos de resposta de APIs externas para um formato interno unificado:

use std::collections::HashMap;

/// Nosso formato interno unificado para um usuario
#[derive(Debug)]
pub struct Usuario {
    pub id: String,
    pub nome: String,
    pub email: String,
    pub avatar_url: Option<String>,
    pub provedor: String,
}

// === API do GitHub ===
pub mod github {
    #[derive(Debug)]
    pub struct GitHubUser {
        pub id: u64,
        pub login: String,
        pub email: Option<String>,
        pub avatar_url: String,
        pub name: Option<String>,
    }
}

// === API do Google ===
pub mod google {
    #[derive(Debug)]
    pub struct GoogleProfile {
        pub sub: String,
        pub given_name: String,
        pub family_name: String,
        pub email: String,
        pub picture: String,
    }
}

// === API propria (legado) ===
pub mod legado {
    use std::collections::HashMap;

    #[derive(Debug)]
    pub struct UsuarioLegado {
        pub campos: HashMap<String, String>,
    }
}

/// Adapter: GitHub -> Usuario
impl From<github::GitHubUser> for Usuario {
    fn from(gh: github::GitHubUser) -> Self {
        Usuario {
            id: format!("github:{}", gh.id),
            nome: gh.name.unwrap_or_else(|| gh.login.clone()),
            email: gh.email.unwrap_or_else(|| format!("{}@users.noreply.github.com", gh.login)),
            avatar_url: Some(gh.avatar_url),
            provedor: "github".to_string(),
        }
    }
}

/// Adapter: Google -> Usuario
impl From<google::GoogleProfile> for Usuario {
    fn from(gp: google::GoogleProfile) -> Self {
        Usuario {
            id: format!("google:{}", gp.sub),
            nome: format!("{} {}", gp.given_name, gp.family_name),
            email: gp.email,
            avatar_url: Some(gp.picture),
            provedor: "google".to_string(),
        }
    }
}

/// Adapter: Legado -> Usuario (com TryFrom para tratar erros)
impl TryFrom<legado::UsuarioLegado> for Usuario {
    type Error = String;

    fn try_from(u: legado::UsuarioLegado) -> Result<Self, Self::Error> {
        let id = u.campos.get("id")
            .ok_or("Campo 'id' ausente no sistema legado")?;
        let nome = u.campos.get("nome_completo")
            .or_else(|| u.campos.get("nome"))
            .ok_or("Campo 'nome' ausente no sistema legado")?;
        let email = u.campos.get("email")
            .ok_or("Campo 'email' ausente no sistema legado")?;

        Ok(Usuario {
            id: format!("legado:{}", id),
            nome: nome.clone(),
            email: email.clone(),
            avatar_url: u.campos.get("foto").cloned(),
            provedor: "legado".to_string(),
        })
    }
}

/// Servico que trabalha com nosso tipo unificado
fn exibir_perfil(usuario: &Usuario) {
    println!("--- Perfil ---");
    println!("  ID: {}", usuario.id);
    println!("  Nome: {}", usuario.nome);
    println!("  Email: {}", usuario.email);
    println!("  Avatar: {:?}", usuario.avatar_url);
    println!("  Provedor: {}", usuario.provedor);
}

fn main() {
    // Usuario do GitHub
    let gh_user = github::GitHubUser {
        id: 12345,
        login: "maria-dev".to_string(),
        email: Some("maria@exemplo.com".to_string()),
        avatar_url: "https://avatars.githubusercontent.com/u/12345".to_string(),
        name: Some("Maria Silva".to_string()),
    };
    let usuario_gh: Usuario = gh_user.into(); // From automatico
    exibir_perfil(&usuario_gh);

    // Usuario do Google
    let google_profile = google::GoogleProfile {
        sub: "1234567890".to_string(),
        given_name: "Joao".to_string(),
        family_name: "Santos".to_string(),
        email: "joao@gmail.com".to_string(),
        picture: "https://lh3.googleusercontent.com/foto.jpg".to_string(),
    };
    let usuario_google: Usuario = google_profile.into();
    exibir_perfil(&usuario_google);

    // Usuario do sistema legado (pode falhar)
    let legado_user = legado::UsuarioLegado {
        campos: HashMap::from([
            ("id".to_string(), "99".to_string()),
            ("nome_completo".to_string(), "Ana Costa".to_string()),
            ("email".to_string(), "ana@empresa.com.br".to_string()),
        ]),
    };
    match Usuario::try_from(legado_user) {
        Ok(usuario) => exibir_perfil(&usuario),
        Err(e) => println!("Erro na conversao: {}", e),
    }
}

Quando Usar

  • Integrar bibliotecas externas cujos tipos nao sao compativeis com seu sistema
  • Regra do orfao impede implementar traits diretamente - use Newtype
  • Migracoes graduais - adaptar APIs legadas para novas interfaces
  • Unificar formatos de multiplas fontes externas (APIs, bancos, arquivos)
  • Conversoes de tipo com From/Into/TryFrom/TryInto

Quando NAO Usar

  • Tipos que voce controla - modifique diretamente em vez de criar wrappers
  • Muitos metodos para delegar - o boilerplate de Newtype pode ser excessivo
  • Conversoes simples que uma funcao livre resolve melhor
  • Performance critica onde a indirecao (mesmo que zero-cost) complica a otimizacao

Variacoes em Rust

1. Newtype com Deref (delegacao automatica)

use std::ops::Deref;

/// Newtype que delega metodos automaticamente via Deref
pub struct Celsius(f64);

impl Deref for Celsius {
    type Target = f64;
    fn deref(&self) -> &f64 {
        &self.0
    }
}

// Agora Celsius tem todos os metodos de f64:
// celsius.abs(), celsius.round(), etc.

2. Adapter com closure

/// Adapter funcional: transforma qualquer funcao para uma interface
fn adaptar_comparador<T>(
    comparar: impl Fn(&T, &T) -> bool,
) -> impl Fn(&T, &T) -> std::cmp::Ordering {
    move |a, b| {
        if comparar(a, b) {
            std::cmp::Ordering::Less
        } else if comparar(b, a) {
            std::cmp::Ordering::Greater
        } else {
            std::cmp::Ordering::Equal
        }
    }
}

3. Adapter via trait com implementacao padrao

/// Trait com metodo adaptador padrao
pub trait Serializavel {
    fn para_json(&self) -> String;

    /// Adaptador embutido: converte para outro formato
    fn para_csv(&self) -> String {
        // Implementacao padrao que adapta JSON para CSV
        let json = self.para_json();
        format!("adaptado_de_json:{}", json)
    }
}

Padroes Relacionados

  • Decorator - Decorator adiciona funcionalidade; Adapter muda a interface
  • Facade - Facade simplifica uma interface complexa; Adapter torna compativel
  • Strategy - Strategies podem ser adaptadas via Newtype para diferentes interfaces
  • Factory - Factory pode retornar adapters para tipos externos

Conclusao

O Adapter em Rust se beneficia enormemente do Newtype Pattern, que e uma idioma central da linguagem. O custo zero em tempo de execucao do Newtype torna o Adapter praticamente “gratis” em termos de performance. As traits From, Into, TryFrom e TryInto da biblioteca padrao fornecem um mecanismo padronizado e ergonomico para adaptacao de tipos. A regra do orfao, que a principio parece restritiva, na verdade guia o desenvolvedor a criar abstraccoes bem definidas e desacopladas.