AgentSkillsCN

rust-architecture

以清晰的架构、明确的模块划分与良好的编码实践,组织 Rust 项目。适用于创建 Rust 项目、重构现有代码,或确立代码组织规范时使用。

SKILL.md
--- frontmatter
name: rust-architecture
description: Organiza projetos Rust com arquitetura limpa, módulos bem definidos e boas práticas. Use para criar projetos Rust, refatorar código existente, ou estabelecer padrões de organização.

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 ? ou expect com mensagem)
  • clone() desnecessário
  • ❌ Tipos genéricos excessivos
  • ❌ Lifetimes complexos quando Arc/Box resolvem
  • pub em tudo
  • ❌ Módulos gigantes (max ~300 linhas)
  • ❌ Panic em bibliotecas (retornar Result)