Rust Architecture Expert
Skill para criar e manter projetos Rust com arquitetura limpa e idiomática.
Estrutura de Projeto
Biblioteca (lib)
code
my-lib/
├── Cargo.toml
├── src/
│ ├── lib.rs # Ponto de entrada, re-exports públicos
│ ├── domain/
│ │ ├── mod.rs
│ │ ├── entities/
│ │ │ ├── mod.rs
│ │ │ └── produto.rs
│ │ ├── repositories/ # Traits (interfaces)
│ │ │ ├── mod.rs
│ │ │ └── produto_repository.rs
│ │ └── errors.rs # Erros de domínio
│ ├── application/
│ │ ├── mod.rs
│ │ ├── use_cases/
│ │ │ ├── mod.rs
│ │ │ └── criar_produto.rs
│ │ └── dtos/
│ │ ├── mod.rs
│ │ └── produto_dto.rs
│ ├── infrastructure/
│ │ ├── mod.rs
│ │ └── repositories/
│ │ ├── mod.rs
│ │ └── sqlite_produto_repository.rs
│ └── prelude.rs # Re-exports convenientes
└── tests/
└── integration/
Binário (bin)
code
my-app/ ├── Cargo.toml ├── src/ │ ├── main.rs │ ├── cli/ # Se CLI │ │ ├── mod.rs │ │ └── commands/ │ ├── api/ # Se HTTP server │ │ ├── mod.rs │ │ ├── routes/ │ │ └── handlers/ │ └── config.rs └── tests/
Workspace (múltiplos crates)
code
workspace/ ├── Cargo.toml # [workspace] members ├── crates/ │ ├── core/ # Domínio puro │ │ ├── Cargo.toml │ │ └── src/ │ ├── infrastructure/ # Implementações │ │ ├── Cargo.toml │ │ └── src/ │ └── api/ # HTTP/CLI │ ├── Cargo.toml │ └── src/ └── tests/ # Integration tests
Padrões de Código
Entity
rust
// domain/entities/produto.rs
use serde::{Deserialize, Serialize};
use uuid::Uuid;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Produto {
id: Uuid,
nome: String,
preco: f64,
}
impl Produto {
pub fn new(nome: impl Into<String>, preco: f64) -> Result<Self, DomainError> {
let nome = nome.into();
if nome.len() < 3 {
return Err(DomainError::InvalidName("Nome deve ter pelo menos 3 caracteres"));
}
if preco <= 0.0 {
return Err(DomainError::InvalidPrice("Preço deve ser positivo"));
}
Ok(Self {
id: Uuid::new_v4(),
nome,
preco,
})
}
// Getters
pub fn id(&self) -> Uuid { self.id }
pub fn nome(&self) -> &str { &self.nome }
pub fn preco(&self) -> f64 { self.preco }
// Comportamentos
pub fn aplicar_desconto(&mut self, percentual: f64) -> Result<(), DomainError> {
if !(0.0..=100.0).contains(&percentual) {
return Err(DomainError::InvalidDiscount);
}
self.preco *= 1.0 - (percentual / 100.0);
Ok(())
}
}
Repository Trait
rust
// domain/repositories/produto_repository.rs
use async_trait::async_trait;
use crate::domain::{entities::Produto, errors::DomainError};
use uuid::Uuid;
#[async_trait]
pub trait ProdutoRepository: Send + Sync {
async fn save(&self, produto: &Produto) -> Result<(), DomainError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<Produto>, DomainError>;
async fn find_all(&self) -> Result<Vec<Produto>, DomainError>;
async fn delete(&self, id: Uuid) -> Result<(), DomainError>;
}
Use Case
rust
// application/use_cases/criar_produto.rs
use crate::domain::{
entities::Produto,
repositories::ProdutoRepository,
errors::DomainError,
};
use crate::application::dtos::CriarProdutoDto;
use std::sync::Arc;
pub struct CriarProdutoUseCase {
repository: Arc<dyn ProdutoRepository>,
}
impl CriarProdutoUseCase {
pub fn new(repository: Arc<dyn ProdutoRepository>) -> Self {
Self { repository }
}
pub async fn execute(&self, dto: CriarProdutoDto) -> Result<Produto, DomainError> {
let produto = Produto::new(dto.nome, dto.preco)?;
self.repository.save(&produto).await?;
Ok(produto)
}
}
Error Handling
rust
// domain/errors.rs
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DomainError {
#[error("Nome inválido: {0}")]
InvalidName(&'static str),
#[error("Preço inválido: {0}")]
InvalidPrice(&'static str),
#[error("Desconto inválido")]
InvalidDiscount,
#[error("Entidade não encontrada: {0}")]
NotFound(String),
#[error("Erro de persistência: {0}")]
Persistence(#[from] sqlx::Error),
#[error("Erro interno: {0}")]
Internal(String),
}
Módulos (mod.rs)
rust
// domain/mod.rs pub mod entities; pub mod repositories; pub mod errors; pub use entities::*; pub use repositories::*; pub use errors::DomainError;
Cargo.toml Patterns
Workspace
toml
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Seu Nome <email@exemplo.com>"]
license = "MIT"
[workspace.dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
thiserror = "1"
anyhow = "1"
uuid = { version = "1", features = ["v4", "serde"] }
Crate
toml
[package] name = "my-crate" version.workspace = true edition.workspace = true [dependencies] tokio.workspace = true serde.workspace = true thiserror.workspace = true [dev-dependencies] tokio-test = "0.4" mockall = "0.11"
Boas Práticas
Visibility
rust
// Usar pub(crate) para itens internos
pub(crate) struct InternalHelper;
// pub apenas para API pública
pub struct PublicApi;
// pub(super) para visibilidade no módulo pai
pub(super) fn helper_function() {}
Builder Pattern
rust
#[derive(Default)]
pub struct ProdutoBuilder {
nome: Option<String>,
preco: Option<f64>,
}
impl ProdutoBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn nome(mut self, nome: impl Into<String>) -> Self {
self.nome = Some(nome.into());
self
}
pub fn preco(mut self, preco: f64) -> Self {
self.preco = Some(preco);
self
}
pub fn build(self) -> Result<Produto, DomainError> {
let nome = self.nome.ok_or(DomainError::InvalidName("Nome é obrigatório"))?;
let preco = self.preco.ok_or(DomainError::InvalidPrice("Preço é obrigatório"))?;
Produto::new(nome, preco)
}
}
Newtype Pattern
rust
// Para tipos com semântica específica
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Price(f64);
impl Price {
pub fn new(value: f64) -> Result<Self, DomainError> {
if value < 0.0 {
return Err(DomainError::InvalidPrice("Preço não pode ser negativo"));
}
Ok(Self(value))
}
pub fn value(&self) -> f64 {
self.0
}
}
Testes
rust
#[cfg(test)]
mod tests {
use super::*;
use mockall::predicate::*;
// Mock do repository
mockall::mock! {
ProdutoRepo {}
#[async_trait]
impl ProdutoRepository for ProdutoRepo {
async fn save(&self, produto: &Produto) -> Result<(), DomainError>;
async fn find_by_id(&self, id: Uuid) -> Result<Option<Produto>, DomainError>;
async fn find_all(&self) -> Result<Vec<Produto>, DomainError>;
async fn delete(&self, id: Uuid) -> Result<(), DomainError>;
}
}
#[tokio::test]
async fn test_criar_produto_sucesso() {
let mut mock_repo = MockProdutoRepo::new();
mock_repo
.expect_save()
.returning(|_| Ok(()));
let use_case = CriarProdutoUseCase::new(Arc::new(mock_repo));
let dto = CriarProdutoDto {
nome: "Teste".to_string(),
preco: 10.0,
};
let result = use_case.execute(dto).await;
assert!(result.is_ok());
}
}
Comandos
bash
# Criar projeto cargo new my-project cargo new my-lib --lib # Build cargo build --release # Testes cargo test cargo test --workspace # Lint cargo clippy -- -W clippy::pedantic # Format cargo fmt # Docs cargo doc --open # Check sem compilar cargo check
Anti-patterns a Evitar
- •❌
unwrap()em código de produção (usar?ouexpectcom mensagem) - •❌
clone()desnecessário - •❌ Tipos genéricos excessivos
- •❌ Lifetimes complexos quando
Arc/Boxresolvem - •❌
pubem tudo - •❌ Módulos gigantes (max ~300 linhas)
- •❌ Panic em bibliotecas (retornar
Result)