Skip to main content

rust-patterns

Rust: ownership, lifetimes, async (Tokio), Result/anyhow/thiserror, traits, unsafe. Triggers: Rust, borrow checker, lifetime, Tokio, cargo, trait, impl, Result, unsafe, clippy.

Informations de source

Dépôt
softspark/ai-toolkit
Dernière activité de la source
6 mai 2026 à 09:48
Langue détectée de SKILL.md
anglais
Étoiles
179
Forks
21

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
rust-patterns
description
Rust: ownership, lifetimes, async (Tokio), Result/anyhow/thiserror, traits, unsafe. Triggers: Rust, borrow checker, lifetime, Tokio, cargo, trait, impl, Result, unsafe, clippy.
effort
medium
user-invocable
false
allowed-tools
Read
# Rust Patterns ## Project Structure ``` my-app/ ├── Cargo.toml ├── src/ │ ├── main.rs # Binary entry point │ ├── lib.rs # Library root (re-exports) │ ├── error.rs # Crate-level error types │ ├── api/ │ │ ├── mod.rs │ │ └── handlers.rs │ └── domain/ │ ├── mod.rs │ └── service.rs ├── tests/ # Integration tests (separate crate) │ └── api_test.rs ├── benches/ # criterion benchmarks │ └── throughput.rs └── examples/ └── demo.rs ``` Workspace layout for multi-crate projects: ```toml # Cargo.toml (workspace root) [workspace] resolver = "2" members = ["crates/core", "crates/api", "crates/cli"] [workspace.dependencies] serde = { version = "1", features = ["derive"] } tokio = { version = "1", features = ["full"] } ``` --- ## Idioms / Code Style ### Ownership and Borrowing ```rust // Borrow when you only need to read fn print_name(name: &str) { println!("{name}"); } // Take ownership when storing or consuming the value fn register_user(name: String) -> User { User { name, id: Uuid::new_v4() } } ``` ### Lifetimes ```rust // Annotate only when the compiler cannot infer struct Parser<'input> { source: &'input str, pos: usize, } impl<'input> Parser<'input> { fn next_token(&mut self) -> Option<&'input str> { let start = self.pos; // ... advance self.pos ... Some(&self.source[start..self.pos]) } } ``` ### Trait-Based Design ```rust trait Repository { fn find_by_id(&self, id: Uuid) -> Result<Option<User>, DbError>; fn save(&self, user: &User) -> Result<(), DbError>; } // Accept generics for testability fn create_user(repo: &impl Repository, name: String) -> Result<User, AppError> { let user = User::new(name); repo.save(&user)?; Ok(user) } ``` ### Iterators, Pattern Matching, Newtype ```rust // Iterator chains over manual loops let active: Vec<&str> = users.iter() .filter(|u| u.is_active) .map(|u| u.email.as_str()) .collect(); // Exhaustive matching match command { Command::Start { port } => start_server(port), Command::Stop => shutdown(), } // let-else for early exit (Rust 1.65+) let Some(cfg) = load_config() else { return Ok(Config::default()); }; // Newtype to prevent primitive misuse struct UserId(Uuid); struct Email(String); impl Email { fn new(raw: &str) -> Result<Self, ValidationError> { if raw.contains('@') { Ok(Self(raw.to_lowercase())) } else { Err(ValidationError::InvalidEmail) } } } ``` ### Builder Pattern ```rust #[derive(Default)] struct RequestBuilder { url: String, timeout: Option<Duration> } impl RequestBuilder { fn url(mut self, url: impl Into<String>) -> Self { self.url = url.into(); self } fn timeout(mut self, d: Duration) -> Self { self.timeout = Some(d); self } fn build(self) -> Result<Request, BuildError> { if self.url.is_empty() { return Err(BuildError::MissingUrl); } Ok(Request { url: self.url, timeout: self.timeout.unwrap_or(Duration::from_secs(30)) }) } } ``` --- ## Error Handling ### thiserror (libraries) vs anyhow (binaries) ```rust use thiserror::Error; #[derive(Debug, Error)] pub enum AppError { #[error("not found: {0}")] NotFound(String), #[error("validation failed: {0}")] Validation(String), #[error("database error")] Database(#[from] sqlx::Error), #[error(transparent)] Unexpected(#[from] anyhow::Error), } ``` ```rust // anyhow for application / binary code -- adds context to any error use anyhow::{Context, Result}; fn load_config(path: &Path) -> Result<Config> { let content = fs::read_to_string(path) .with_context(|| format!("failed to read {}", path.display()))?; toml::from_str(&content).context("invalid TOML") } ``` ### Error Propagation and Boundary Mapping ```rust // ? converts and propagates via From impls fn get_email(pool: &PgPool, id: Uuid) -> Result<String, AppError> { let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) .fetch_optional(pool).await? .ok_or_else(|| AppError::NotFound(format!("user {id}")))?; Ok(user.email) } // Map domain errors to HTTP at the API boundary impl IntoResponse for AppError { fn into_response(self) -> axum::response::Response { let (status, msg) = match &self { AppError::NotFound(m) => (StatusCode::NOT_FOUND, m.clone()), AppError::Validation(m) => (StatusCode::BAD_REQUEST, m.clone()), _ => (StatusCode::INTERNAL_SERVER_ERROR, "internal error".into()), }; (status, Json(json!({ "error": msg }))).into_response() } } ``` --- ## Testing Patterns ### Unit Tests (inline module) ```rust #[cfg(test)] mod tests { use super::*; #[test] fn email_rejects_invalid() { assert!(Email::new("bad").is_err()); } #[tokio::test] async fn fetches_user() { let pool = setup_test_db().await; let user = get_user(&pool, test_id()).await.unwrap(); assert_eq!(user.name, "Alice"); } } ``` ### Integration Tests (tests/ directory) ```rust // tests/api_test.rs -- compiled as separate crate, only sees pub API #[tokio::test] async fn health_returns_200() { let app = my_app::app().await; let resp = app.oneshot( Request::builder().uri("/health").body(Body::empty()).unwrap() ).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } ``` ### Mocking (mockall) and Property Testing (proptest) ```rust use mockall::automock; #[automock] trait UserRepo { fn find(&self, id: Uuid) -> Result<Option<User>, DbError>; } #[test] fn returns_not_found_when_missing() { let mut mock = MockUserRepo::new(); mock.expect_find().returning(|_| Ok(None)); assert!(matches!(UserService::new(mock).get_user(id), Err(AppError::NotFound(_)))); } ``` ```rust use proptest::prelude::*; proptest! { #[test] fn roundtrips(s in "[a-z]{1,20}@[a-z]{1,10}\\.[a-z]{2,4}") { let e = Email::new(&s).unwrap(); assert_eq!(e.to_string(), s.to_lowercase()); } } ``` ```bash cargo test # All tests cargo test --lib # Unit only cargo test --test api_test # Single integration file cargo nextest run # Parallel runner, better output ``` --- ## Common Libraries | Crate | Purpose | Notes | |-------|---------|-------| | **serde** / serde_json | Serialization | `#[derive(Serialize, Deserialize)]` on DTOs | | **tokio** | Async runtime | `features = ["full"]` for apps | | **axum** | HTTP framework | Tower-based, extractors, `State` | | **clap** | CLI parsing | `#[derive(Parser)]` | | **reqwest** | HTTP client | Async, rustls TLS | | **tracing** | Structured logging | Replaces `log`; use `tracing-subscriber` | | **sqlx** | Async SQL | Compile-time checked queries | | **tower** | Middleware | Layers, timeouts, rate limiting | | **rayon** | Data parallelism | `.par_iter()` drop-in | | **criterion** | Benchmarking | Statistical, HTML reports | ### Axum + Tracing Minimal Server ```rust use axum::{extract::State, routing::get, Json, Router}; use std::sync::Arc; struct AppState { db: PgPool } async fn list_users(State(s): State<Arc<AppState>>) -> Json<Vec<User>> { Json(sqlx::query_as!(User, "SELECT * FROM users") .fetch_all(&s.db).await.unwrap()) } #[tokio::main] async fn main() { tracing_subscriber::fmt() .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) .json().init(); let pool = PgPool::connect(&std::env::var("DATABASE_URL").unwrap()).await.unwrap(); let app = Router::new().route("/users", get(list_users)).with_state(Arc::new(AppState { db: pool })); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); } ``` --- ## Performance Tips ```rust // Avoid cloning -- return references when possible fn names(data: &[Record]) -> Vec<&str> { data.iter().map(|r| r.name.as_str()).collect() } // Cow for conditional ownership -- zero alloc on the happy path use std::borrow::Cow; fn normalize(input: &str) -> Cow<'_, str> { if input.contains(' ') { Cow::Owned(input.replace(' ', "_")) } else { Cow::Borrowed(input) } } // Rayon for CPU-bound parallelism use rayon::prelude::*; let out: Vec<_> = inputs.par_iter().map(|i| compute(i)).collect(); // Preallocate collections let mut v = Vec::with_capacity(items.len()); // Zero-cost abstractions: iterators compile to the same code as manual loops let sum: u64 = values.iter().filter(|v| **v > 0).sum(); // #[inline] only on small hot functions crossing crate boundaries ``` ### Benchmarking (criterion)
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub