
Rust
- 79 installs
- 14 repo stars
- Updated March 2, 2026
- oakoss/agent-skills
Helps with ai & agent building tasks during AI-assisted development.
About
rust is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- rust
- AI & Agent Building
- AI-coding skill
Rust by the numbers
- 79 all-time installs (skills.sh)
- +1 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #5,292 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/oakoss/agent-skills --skill rustAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 79 |
|---|---|
| repo stars | ★ 14 |
| Last updated | March 2, 2026 |
| Repository | oakoss/agent-skills ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Rust
Overview
Rust is a systems programming language focused on safety, concurrency, and performance. The ownership system guarantees memory safety without garbage collection, and the type system enforces thread safety at compile time.
When to use: Systems programming, web services (axum), CLI tools (clap), terminal UIs (ratatui), WebAssembly, performance-critical applications, anything requiring memory safety without runtime overhead.
When NOT to use: Rapid prototyping where compile times matter more than safety, simple scripting tasks, projects where the team has no Rust experience and deadlines are tight.
Quick Reference
| Pattern | API / Tool | Key Points |
|---|---|---|
| Ownership transfer | let b = a; | a is moved, no longer usable |
| Borrowing | &T / &mut T | One mutable OR many immutable refs |
| Lifetime annotation | fn f<'a>(x: &'a str) -> &'a str | Ties output lifetime to input |
| Error propagation | ? operator | Converts and propagates errors |
| Custom errors | thiserror::Error derive | Structured error types with Display |
| Ad-hoc errors | anyhow::Result<T> | Context chaining for applications |
| Async runtime | #[tokio::main] | Entry point for async programs |
| Spawn task | tokio::spawn(async { }) | Concurrent async task execution |
| HTTP router | axum::Router::new().route(...) | Composable routing with extractors |
| Extractors | Json<T>, Path<T>, State<T> | Type-safe request parsing |
| Serialization | #[derive(Serialize, Deserialize)] | serde with format-agnostic derives |
| CLI parsing | #[derive(Parser)] | clap derive API for arg parsing |
| TUI rendering | `terminal.draw(\ | f\ |
| Benchmarking | criterion::Criterion | Statistical benchmarking framework |
| Dep auditing | cargo deny check | License, vulnerability, source audit |
| Binary release | cargo dist init | Cross-platform binary distribution |
| Changelog | release-plz update | Auto semver bump and changelog |
Common Mistakes
| Mistake | Correct Pattern |
|---|---|
| Returning reference to local variable | Return owned type or use lifetime parameter |
Using unwrap() in library code | Return Result and let caller decide |
clone() to satisfy borrow checker | Restructure code to avoid the borrow conflict |
| Blocking in async context | Use tokio::task::spawn_blocking |
| Shared mutable state without sync | Use Arc<Mutex<T>> or channels |
String where &str suffices | Accept &str in function parameters |
Ignoring must_use warnings | Handle or explicitly discard with let _ = |
| Large enum variants | Box the large variant to reduce overall size |
async fn in traits without bounds | Add Send bound or use async-trait crate |
Missing #[tokio::test] on async tests | Use #[tokio::test] instead of #[test] |
Delegation
- Code exploration: Use
Exploreagent - Architecture review: Use
Taskagent - Code review: Delegate to
code-revieweragent
If the docker skill is available, delegate multi-stage container build patterns to it.If the github-actions skill is available, delegate CI pipeline configuration to it.If the openapi skill is available, delegate API specification and code generation to it.References
- Ownership, borrowing, lifetimes, and type system patterns
- Error handling with Result, Option, thiserror, and anyhow
- Async programming with Tokio runtime and concurrency
- Web APIs and backends with axum, tower, and sqlx
- Terminal user interfaces with ratatui and crossterm
- CLI applications with clap, config, and signal handling
- Testing patterns and criterion benchmarking
- Release pipeline with cargo-deny, cargo-dist, and release-plz
Async Tokio
Runtime Setup
Tokio is the most widely used async runtime for Rust. The #[tokio::main] macro transforms async fn main() into a synchronous entry point that starts the runtime.
[dependencies]
tokio = { version = "1", features = ["full"] }#[tokio::main]
async fn main() {
println!("running on tokio");
}
// For libraries that need a minimal runtime
#[tokio::main(flavor = "current_thread")]
async fn main() {
println!("single-threaded runtime");
}Feature Flags
| Feature | Provides |
|---|---|
rt | Core runtime |
rt-multi-thread | Multi-threaded scheduler |
macros | #[tokio::main], #[tokio::test] |
io-util | AsyncReadExt, AsyncWriteExt |
net | TCP, UDP, Unix sockets |
time | sleep, interval, timeout |
sync | Channels, Mutex, RwLock, etc. |
signal | Ctrl+C and OS signal handling |
fs | Async filesystem operations |
full | All features |
Spawning Tasks
tokio::spawn runs a future concurrently on the runtime. The future must be Send + 'static.
use tokio::task::JoinHandle;
async fn fetch_data(url: String) -> Result<String, reqwest::Error> {
reqwest::get(&url).await?.text().await
}
async fn parallel_fetch() {
let handles: Vec<JoinHandle<Result<String, reqwest::Error>>> = vec![
tokio::spawn(fetch_data("https://api.example.com/a".into())),
tokio::spawn(fetch_data("https://api.example.com/b".into())),
];
for handle in handles {
match handle.await {
Ok(Ok(body)) => println!("got {} bytes", body.len()),
Ok(Err(e)) => eprintln!("request failed: {e}"),
Err(e) => eprintln!("task panicked: {e}"),
}
}
}spawn_blocking for CPU-bound Work
async fn hash_password(password: String) -> Result<String, anyhow::Error> {
tokio::task::spawn_blocking(move || {
bcrypt::hash(password, bcrypt::DEFAULT_COST)
.map_err(|e| anyhow::anyhow!("hash failed: {e}"))
})
.await?
}Channels
mpsc (Multi-Producer, Single-Consumer)
use tokio::sync::mpsc;
#[derive(Debug)]
enum Command {
Get { key: String, resp: tokio::sync::oneshot::Sender<Option<String>> },
Set { key: String, value: String },
}
async fn run_store(mut rx: mpsc::Receiver<Command>) {
let mut store = std::collections::HashMap::new();
while let Some(cmd) = rx.recv().await {
match cmd {
Command::Get { key, resp } => {
let _ = resp.send(store.get(&key).cloned());
}
Command::Set { key, value } => {
store.insert(key, value);
}
}
}
}
async fn main_task() {
let (tx, rx) = mpsc::channel(32);
tokio::spawn(run_store(rx));
tx.send(Command::Set {
key: "foo".into(),
value: "bar".into(),
}).await.unwrap();
let (resp_tx, resp_rx) = tokio::sync::oneshot::channel();
tx.send(Command::Get {
key: "foo".into(),
resp: resp_tx,
}).await.unwrap();
let value = resp_rx.await.unwrap();
println!("got: {value:?}");
}broadcast (Multi-Producer, Multi-Consumer)
use tokio::sync::broadcast;
async fn event_system() {
let (tx, _) = broadcast::channel::<String>(16);
let mut rx1 = tx.subscribe();
let mut rx2 = tx.subscribe();
tokio::spawn(async move {
while let Ok(msg) = rx1.recv().await {
println!("subscriber 1: {msg}");
}
});
tokio::spawn(async move {
while let Ok(msg) = rx2.recv().await {
println!("subscriber 2: {msg}");
}
});
tx.send("event happened".into()).unwrap();
}watch (Single-Producer, Multi-Consumer, Latest Value)
use tokio::sync::watch;
async fn config_reload() {
let (tx, mut rx) = watch::channel(AppConfig::default());
tokio::spawn(async move {
while rx.changed().await.is_ok() {
let config = rx.borrow().clone();
println!("config updated: {config:?}");
}
});
tx.send(AppConfig { port: 8080, ..Default::default() }).unwrap();
}select! Macro
tokio::select! waits on multiple async expressions and executes the branch that completes first.
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
async fn process_with_timeout(mut rx: mpsc::Receiver<String>) {
loop {
tokio::select! {
Some(msg) = rx.recv() => {
println!("received: {msg}");
}
_ = sleep(Duration::from_secs(30)) => {
println!("no message for 30s, shutting down");
break;
}
}
}
}Graceful Shutdown
use tokio::signal;
use tokio::sync::watch;
async fn run_server() -> anyhow::Result<()> {
let (shutdown_tx, mut shutdown_rx) = watch::channel(false);
let server = tokio::spawn(async move {
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
loop {
tokio::select! {
Ok((stream, addr)) = listener.accept() => {
let mut rx = shutdown_rx.clone();
tokio::spawn(async move {
tokio::select! {
_ = handle_connection(stream, addr) => {}
_ = rx.changed() => {
println!("connection handler shutting down");
}
}
});
}
_ = shutdown_rx.changed() => {
println!("server shutting down");
break;
}
}
}
});
signal::ctrl_c().await?;
println!("shutdown signal received");
let _ = shutdown_tx.send(true);
server.await?;
Ok(())
}Timeouts and Intervals
use tokio::time::{timeout, interval, sleep, Duration};
async fn with_timeout() -> anyhow::Result<String> {
let result = timeout(Duration::from_secs(5), fetch_data())
.await
.map_err(|_| anyhow::anyhow!("operation timed out"))??;
Ok(result)
}
async fn periodic_task() {
let mut ticker = interval(Duration::from_secs(60));
loop {
ticker.tick().await;
println!("running periodic cleanup");
}
}Synchronization Primitives
| Primitive | Use Case |
|---|---|
tokio::sync::Mutex | Async-aware mutual exclusion |
tokio::sync::RwLock | Multiple readers, single writer |
tokio::sync::Semaphore | Limit concurrent access |
tokio::sync::Notify | Wake one or all waiters |
tokio::sync::Barrier | Synchronize multiple tasks |
use std::sync::Arc;
use tokio::sync::Semaphore;
async fn rate_limited_fetch(urls: Vec<String>) {
let semaphore = Arc::new(Semaphore::new(10));
let mut handles = vec![];
for url in urls {
let permit = semaphore.clone().acquire_owned().await.unwrap();
handles.push(tokio::spawn(async move {
let result = reqwest::get(&url).await;
drop(permit);
result
}));
}
for handle in handles {
let _ = handle.await;
}
}Axum Web
Project Setup
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tower = "0.5"
tower-http = { version = "0.6", features = ["cors", "trace", "compression-gzip"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }Router and Handlers
use axum::{
extract::{Json, Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::{get, post, put, delete},
Router,
};
use serde::{Deserialize, Serialize};
#[derive(Clone)]
struct AppState {
db: sqlx::PgPool,
}
#[tokio::main]
async fn main() {
tracing_subscriber::init();
let pool = sqlx::PgPool::connect("postgres://localhost/mydb")
.await
.expect("failed to connect to database");
let state = AppState { db: pool };
let app = Router::new()
.route("/health", get(health))
.nest("/api/v1", api_routes())
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
fn api_routes() -> Router<AppState> {
Router::new()
.route("/users", get(list_users).post(create_user))
.route("/users/{id}", get(get_user).put(update_user).delete(delete_user))
}
async fn health() -> &'static str {
"ok"
}Extractors
Extractors parse request data into typed values. Order matters: body-consuming extractors must be last.
#[derive(Deserialize)]
struct Pagination {
#[serde(default = "default_page")]
page: u32,
#[serde(default = "default_per_page")]
per_page: u32,
}
fn default_page() -> u32 { 1 }
fn default_per_page() -> u32 { 20 }
async fn list_users(
State(state): State<AppState>,
Query(pagination): Query<Pagination>,
) -> Result<Json<Vec<User>>, ApiError> {
let offset = (pagination.page - 1) * pagination.per_page;
let users = sqlx::query_as!(
User,
"SELECT id, name, email FROM users LIMIT $1 OFFSET $2",
pagination.per_page as i64,
offset as i64
)
.fetch_all(&state.db)
.await?;
Ok(Json(users))
}
#[derive(Deserialize)]
struct CreateUser {
name: String,
email: String,
}
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> Result<(StatusCode, Json<User>), ApiError> {
let user = sqlx::query_as!(
User,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
payload.name,
payload.email
)
.fetch_one(&state.db)
.await?;
Ok((StatusCode::CREATED, Json(user)))
}
async fn get_user(
State(state): State<AppState>,
Path(id): Path<i64>,
) -> Result<Json<User>, ApiError> {
let user = sqlx::query_as!(User, "SELECT id, name, email FROM users WHERE id = $1", id)
.fetch_optional(&state.db)
.await?
.ok_or(ApiError::NotFound)?;
Ok(Json(user))
}Middleware with Tower
Layer-Based Middleware
use axum::Router;
use tower::ServiceBuilder;
use tower_http::{
cors::{Any, CorsLayer},
compression::CompressionLayer,
trace::TraceLayer,
};
use std::time::Duration;
fn app() -> Router<AppState> {
Router::new()
.nest("/api", api_routes())
.layer(
ServiceBuilder::new()
.layer(TraceLayer::new_for_http())
.layer(CompressionLayer::new())
.layer(
CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any)
.max_age(Duration::from_secs(3600)),
),
)
}Custom Middleware with from_fn
use axum::{
extract::Request,
http::{header, StatusCode},
middleware::{self, Next},
response::Response,
};
async fn auth_middleware(
request: Request,
next: Next,
) -> Result<Response, StatusCode> {
let auth_header = request
.headers()
.get(header::AUTHORIZATION)
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::UNAUTHORIZED)?;
if !auth_header.starts_with("Bearer ") {
return Err(StatusCode::UNAUTHORIZED);
}
let token = &auth_header[7..];
validate_token(token)
.map_err(|_| StatusCode::UNAUTHORIZED)?;
Ok(next.run(request).await)
}
fn protected_routes() -> Router<AppState> {
Router::new()
.route("/profile", get(profile))
.route("/settings", get(settings).put(update_settings))
.layer(middleware::from_fn(auth_middleware))
}sqlx Database Access
[dependencies]
sqlx = { version = "0.8", features = ["runtime-tokio", "postgres", "migrate"] }use sqlx::PgPool;
#[derive(Debug, sqlx::FromRow, Serialize)]
struct User {
id: i64,
name: String,
email: String,
}
async fn setup_pool() -> PgPool {
PgPool::connect_lazy("postgres://localhost/mydb")
.expect("invalid database URL")
}Migrations
# Install sqlx-cli
cargo install sqlx-cli --no-default-features --features postgres
# Create and run migrations
sqlx migrate add create_users
sqlx migrate run
# Verify queries at compile time
cargo sqlx prepare-- migrations/001_create_users.sql
CREATE TABLE users (
id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);Tracing and Observability
use tracing::{info, warn, error, instrument};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
fn init_tracing() {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into()))
.with(tracing_subscriber::fmt::layer())
.init();
}
#[instrument(skip(state))]
async fn create_user(
State(state): State<AppState>,
Json(payload): Json<CreateUser>,
) -> Result<Json<User>, ApiError> {
info!(name = %payload.name, "creating user");
let user = sqlx::query_as!(
User,
"INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id, name, email",
payload.name,
payload.email
)
.fetch_one(&state.db)
.await
.map_err(|e| {
error!(error = %e, "failed to create user");
ApiError::Internal(e.into())
})?;
info!(user_id = user.id, "user created");
Ok(Json(user))
}Graceful Shutdown with Axum
use tokio::signal;
#[tokio::main]
async fn main() {
let app = Router::new().route("/", get(|| async { "hello" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app)
.with_graceful_shutdown(shutdown_signal())
.await
.unwrap();
}
async fn shutdown_signal() {
let ctrl_c = async {
signal::ctrl_c().await.expect("failed to listen for ctrl+c");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install signal handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => {},
_ = terminate => {},
}
}CLI Applications
Clap Argument Parsing
Clap provides derive-based and builder-based APIs for parsing command-line arguments. The derive API is recommended for most use cases.
[dependencies]
clap = { version = "4", features = ["derive", "env"] }use clap::{Parser, Subcommand, Args, ValueEnum};
#[derive(Parser)]
#[command(name = "myapp", version, about = "A sample CLI application")]
struct Cli {
/// Enable verbose output
#[arg(short, long, global = true)]
verbose: bool,
/// Config file path
#[arg(short, long, default_value = "config.toml", env = "MYAPP_CONFIG")]
config: String,
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Start the server
Serve(ServeArgs),
/// Run database migrations
Migrate {
/// Migration direction
#[arg(value_enum, default_value_t = Direction::Up)]
direction: Direction,
},
/// Generate shell completions
Completions {
/// Shell to generate for
#[arg(value_enum)]
shell: clap_complete::Shell,
},
}
#[derive(Args)]
struct ServeArgs {
/// Port to listen on
#[arg(short, long, default_value_t = 3000, env = "PORT")]
port: u16,
/// Host to bind to
#[arg(long, default_value = "0.0.0.0")]
host: String,
}
#[derive(Clone, ValueEnum)]
enum Direction {
Up,
Down,
}
fn main() {
let cli = Cli::parse();
match cli.command {
Commands::Serve(args) => {
println!("Starting server on {}:{}", args.host, args.port);
}
Commands::Migrate { direction } => {
println!("Running migrations");
}
Commands::Completions { shell } => {
clap_complete::generate(
shell,
&mut Cli::command(),
"myapp",
&mut std::io::stdout(),
);
}
}
}Common Clap Attributes
| Attribute | Effect |
|---|---|
#[arg(short, long)] | Enable -v and --verbose |
#[arg(default_value_t = 8080)] | Default value with Display |
#[arg(env = "PORT")] | Read from environment variable |
#[arg(value_enum)] | Parse from predefined values |
#[arg(num_args = 1..)] | Accept multiple values |
#[arg(required = true)] | Mandatory argument |
#[arg(hide = true)] | Hide from help output |
#[command(subcommand)] | Nested subcommands |
Configuration with config Crate
[dependencies]
config = "0.14"
serde = { version = "1", features = ["derive"] }use config::{Config, Environment, File};
use serde::Deserialize;
#[derive(Debug, Deserialize)]
pub struct AppConfig {
pub server: ServerConfig,
pub database: DatabaseConfig,
pub log_level: String,
}
#[derive(Debug, Deserialize)]
pub struct ServerConfig {
pub host: String,
pub port: u16,
}
#[derive(Debug, Deserialize)]
pub struct DatabaseConfig {
pub url: String,
pub max_connections: u32,
}
impl AppConfig {
pub fn load(config_path: &str) -> Result<Self, config::ConfigError> {
Config::builder()
.set_default("server.host", "0.0.0.0")?
.set_default("server.port", 3000)?
.set_default("database.max_connections", 10)?
.set_default("log_level", "info")?
.add_source(File::with_name(config_path).required(false))
.add_source(Environment::with_prefix("APP").separator("__"))
.build()?
.try_deserialize()
}
}Logging with tracing
[dependencies]
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] }use tracing::{info, warn, error, debug, instrument, Level};
use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
fn init_logging(json: bool) {
let env_filter = EnvFilter::try_from_default_env()
.unwrap_or_else(|_| EnvFilter::new("info"));
let registry = tracing_subscriber::registry().with(env_filter);
if json {
registry.with(fmt::layer().json()).init();
} else {
registry.with(fmt::layer().pretty()).init();
}
}
#[instrument(skip(password))]
fn authenticate(username: &str, password: &str) -> Result<User, AuthError> {
info!(username, "attempting authentication");
let user = find_user(username).map_err(|e| {
warn!(username, error = %e, "user lookup failed");
AuthError::NotFound
})?;
debug!(user_id = user.id, "user found, verifying password");
Ok(user)
}Signal Handling
use tokio::signal;
async fn wait_for_shutdown() {
let ctrl_c = async {
signal::ctrl_c().await.expect("failed to listen for ctrl+c");
};
#[cfg(unix)]
let terminate = async {
signal::unix::signal(signal::unix::SignalKind::terminate())
.expect("failed to install SIGTERM handler")
.recv()
.await;
};
#[cfg(not(unix))]
let terminate = std::future::pending::<()>();
tokio::select! {
_ = ctrl_c => println!("received SIGINT"),
_ = terminate => println!("received SIGTERM"),
}
}FFI (Foreign Function Interface)
// Exposing Rust to C
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub extern "C" fn free_string(ptr: *mut std::ffi::c_char) {
if ptr.is_null() {
return;
}
unsafe {
let _ = std::ffi::CString::from_raw(ptr);
}
}# Cargo.toml for cdylib output
[lib]
crate-type = ["cdylib"]WASM Compilation
[dependencies]
wasm-bindgen = "0.2"
[lib]
crate-type = ["cdylib"]use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn greet(name: &str) -> String {
format!("Hello, {name}!")
}
#[wasm_bindgen]
pub struct Counter {
value: u32,
}
#[wasm_bindgen]
impl Counter {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self { value: 0 }
}
pub fn increment(&mut self) {
self.value += 1;
}
pub fn get(&self) -> u32 {
self.value
}
}# Build for WASM
rustup target add wasm32-unknown-unknown
cargo install wasm-pack
wasm-pack build --target webApplication Bootstrap Pattern
use anyhow::Result;
use clap::Parser;
#[derive(Parser)]
struct Cli {
#[arg(short, long, default_value = "config.toml")]
config: String,
}
#[tokio::main]
async fn main() -> Result<()> {
let cli = Cli::parse();
let config = AppConfig::load(&cli.config)?;
init_logging(false);
tracing::info!("starting application");
let db = setup_database(&config.database).await?;
let state = AppState { db, config };
let app = build_router(state);
let listener = tokio::net::TcpListener::bind(
format!("{}:{}", config.server.host, config.server.port)
).await?;
tracing::info!(
host = %config.server.host,
port = config.server.port,
"server listening"
);
axum::serve(listener, app)
.with_graceful_shutdown(wait_for_shutdown())
.await?;
tracing::info!("server shut down gracefully");
Ok(())
}Error Handling
Result and Option
Result<T, E> represents success or failure. Option<T> represents presence or absence. Both support the ? operator for early return.
use std::fs;
use std::num::ParseIntError;
fn read_age(path: &str) -> Result<u32, Box<dyn std::error::Error>> {
let contents = fs::read_to_string(path)?;
let age: u32 = contents.trim().parse()?;
Ok(age)
}
fn find_user(users: &[&str], name: &str) -> Option<usize> {
users.iter().position(|&u| u == name)
}Combinators
fn process() {
let port: u16 = std::env::var("PORT")
.ok() // Result -> Option
.and_then(|v| v.parse().ok())
.unwrap_or(3000);
let result: Result<i32, String> = Ok(5);
let mapped = result
.map(|v| v * 2) // Transform success value
.map_err(|e| format!("failed: {e}")); // Transform error
let fallback = "42"
.parse::<i32>()
.or_else(|_| "0".parse::<i32>());
}thiserror for Library Errors
thiserror generates Display and Error implementations via derive macros. Use for libraries where callers need to match on error variants.
[dependencies]
thiserror = "2"use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("validation failed: {field}")]
Validation { field: String },
#[error("resource {id} not found")]
NotFound { id: String },
#[error("authentication required")]
Unauthorized,
#[error(transparent)]
Unexpected(#[from] anyhow::Error),
}Composing Error Types
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ConfigError {
#[error("failed to read config file")]
Io(#[from] std::io::Error),
#[error("failed to parse config")]
Parse(#[from] toml::de::Error),
#[error("missing required field: {0}")]
MissingField(String),
}
pub fn load_config(path: &str) -> Result<Config, ConfigError> {
let contents = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&contents)?;
if config.database_url.is_empty() {
return Err(ConfigError::MissingField("database_url".into()));
}
Ok(config)
}anyhow for Application Errors
anyhow provides a single error type with context chaining. Use in application code (binaries, CLI tools) where error matching is not needed.
[dependencies]
anyhow = "1"use anyhow::{bail, ensure, Context, Result};
fn connect_database(url: &str) -> Result<Database> {
ensure!(!url.is_empty(), "database URL must not be empty");
let db = Database::connect(url)
.context("failed to connect to database")?;
if !db.is_healthy() {
bail!("database health check failed");
}
Ok(db)
}
fn load_and_validate(path: &str) -> Result<Config> {
let config = std::fs::read_to_string(path)
.with_context(|| format!("failed to read config from {path}"))?;
let parsed: Config = toml::from_str(&config)
.context("invalid TOML in config file")?;
Ok(parsed)
}anyhow vs thiserror Decision
| Scenario | Use |
|---|---|
| Library crate (published API) | thiserror with typed error enum |
| Binary / application | anyhow::Result with context |
| Internal modules in a binary | anyhow::Result |
| FFI boundary | Custom error codes, not Result |
Axum Error Integration
Convert application errors to HTTP responses using IntoResponse.
use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
#[derive(Debug, thiserror::Error)]
pub enum ApiError {
#[error("not found: {0}")]
NotFound(String),
#[error("validation: {0}")]
Validation(String),
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (status, message) = match &self {
ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
ApiError::Validation(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
ApiError::Internal(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"internal server error".into(),
),
};
(status, Json(json!({ "error": message }))).into_response()
}
}
async fn get_user(
axum::extract::Path(id): axum::extract::Path<String>,
) -> Result<Json<User>, ApiError> {
let user = find_user(&id)
.ok_or_else(|| ApiError::NotFound(format!("user {id}")))?;
Ok(Json(user))
}Pattern: Converting Between Error Types
use std::num::ParseIntError;
#[derive(Debug, thiserror::Error)]
enum MyError {
#[error("parse error: {0}")]
Parse(#[from] ParseIntError),
#[error("out of range: {value} (max {max})")]
OutOfRange { value: i64, max: i64 },
}
fn parse_bounded(s: &str, max: i64) -> Result<i64, MyError> {
let value: i64 = s.parse()?; // ParseIntError auto-converts via From
if value > max {
return Err(MyError::OutOfRange { value, max });
}
Ok(value)
}Unwrap Guidelines
| Method | Use When |
|---|---|
unwrap() | Tests, prototypes, or provably safe cases |
expect("reason") | Invariants with documentation of why it cannot fail |
unwrap_or(default) | Sensible default exists |
| `unwrap_or_else(\ | \ |
? operator | Propagating errors to the caller |
Ownership and Types
Ownership Rules
Every value has exactly one owner. When the owner goes out of scope, the value is dropped. Assignment moves ownership for non-Copy types.
fn main() {
let s1 = String::from("hello");
let s2 = s1; // s1 is moved to s2
// println!("{s1}"); // compile error: value used after move
let n1: i32 = 42;
let n2 = n1; // i32 implements Copy, so n1 is still valid
println!("{n1} {n2}");
}Borrowing
References borrow values without taking ownership. The borrow checker enforces: one mutable reference XOR any number of immutable references.
fn calculate_length(s: &str) -> usize {
s.len()
}
fn append_greeting(s: &mut String) {
s.push_str(", world!");
}
fn main() {
let mut name = String::from("hello");
let len = calculate_length(&name);
append_greeting(&mut name);
println!("{name} has original length {len}");
}Lifetimes
Lifetime annotations describe the relationship between reference lifetimes. The compiler uses them to ensure references remain valid.
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 {
3
}
fn announce(&self, announcement: &str) -> &'a str {
println!("Attention: {announcement}");
self.text
}
}Lifetime Elision Rules
The compiler infers lifetimes in three cases:
1. Each reference parameter gets its own lifetime 2. If exactly one input lifetime, it applies to all output references 3. If &self or &mut self is a parameter, its lifetime applies to output references
// Elided: fn first_word(s: &str) -> &str
// Expanded: fn first_word<'a>(s: &'a str) -> &'a str
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &byte) in bytes.iter().enumerate() {
if byte == b' ' {
return &s[..i];
}
}
s
}Traits and Generics
Traits define shared behavior. Use trait bounds to constrain generic types.
trait Summary {
fn summarize(&self) -> String;
fn preview(&self) -> String {
format!("Read more: {}", self.summarize())
}
}
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, &self.content[..50])
}
}
fn notify(item: &impl Summary) {
println!("Breaking: {}", item.summarize());
}
// Equivalent with trait bound syntax
fn notify_bounded<T: Summary>(item: &T) {
println!("Breaking: {}", item.summarize());
}
// Multiple bounds with where clause
fn process<T>(item: &T) -> String
where
T: Summary + std::fmt::Display,
{
format!("{item}: {}", item.summarize())
}Common Derive Traits
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum Priority {
Low,
Medium,
High,
}
#[derive(Default)]
struct Config {
retries: u32,
timeout_ms: u64,
verbose: bool,
}Enums and Pattern Matching
enum Command {
Quit,
Echo(String),
Move { x: i32, y: i32 },
Color(u8, u8, u8),
}
fn handle(cmd: Command) {
match cmd {
Command::Quit => std::process::exit(0),
Command::Echo(msg) => println!("{msg}"),
Command::Move { x, y } => println!("Moving to ({x}, {y})"),
Command::Color(r, g, b) => println!("#{r:02x}{g:02x}{b:02x}"),
}
}Serde Serialization
Serde provides format-agnostic serialization. Derive Serialize and Deserialize, then use format crates like serde_json, serde_yaml, or toml.
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
email: String,
#[serde(default)]
active: bool,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct ApiResponse<T> {
status_code: u16,
data: T,
#[serde(skip_serializing_if = "Option::is_none")]
error_message: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload")]
enum Event {
UserCreated(User),
UserDeleted { id: u64 },
SystemMessage(String),
}
fn main() -> serde_json::Result<()> {
let user = User {
name: "Alice".into(),
email: "alice@example.com".into(),
active: true,
};
let json = serde_json::to_string_pretty(&user)?;
println!("{json}");
let parsed: User = serde_json::from_str(&json)?;
println!("{parsed:?}");
Ok(())
}Common Serde Attributes
| Attribute | Effect |
|---|---|
#[serde(rename_all = "camelCase")] | Rename all fields to camelCase |
#[serde(default)] | Use Default::default() if missing |
#[serde(skip_serializing_if = "Option::is_none")] | Omit None fields |
#[serde(flatten)] | Inline nested struct fields |
#[serde(tag = "type")] | Internally tagged enum representation |
#[serde(untagged)] | Try each variant in order |
#[serde(deny_unknown_fields)] | Reject unexpected fields |
Cargo Workspace Management
Workspaces share a single Cargo.lock and output directory across multiple crates.
# Root Cargo.toml
[workspace]
members = ["crates/*"]
resolver = "2"
[workspace.package]
edition = "2021"
license = "MIT"
repository = "https://github.com/org/project"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tracing = "0.1"# crates/my-lib/Cargo.toml
[package]
name = "my-lib"
version = "0.1.0"
edition.workspace = true
license.workspace = true
[dependencies]
serde.workspace = trueSmart Pointers
| Type | Use Case |
|---|---|
Box<T> | Heap allocation, recursive types, trait objects |
Rc<T> | Single-threaded shared ownership |
Arc<T> | Thread-safe shared ownership |
Cell<T> / RefCell<T> | Interior mutability (single-threaded) |
Mutex<T> / RwLock<T> | Interior mutability (thread-safe) |
Cow<'a, T> | Clone-on-write, avoid unnecessary allocation |
use std::sync::Arc;
use tokio::sync::Mutex;
#[derive(Clone)]
struct AppState {
db: Arc<DatabasePool>,
cache: Arc<Mutex<HashMap<String, String>>>,
}Ratatui TUI
Project Setup
[dependencies]
ratatui = "0.29"
crossterm = "0.28"
color-eyre = "0.6"Application Scaffold
Ratatui uses an immediate-mode rendering model. Each frame, the entire UI is redrawn from application state.
use std::io;
use ratatui::{
DefaultTerminal, Frame,
crossterm::event::{self, Event, KeyCode, KeyEventKind},
};
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let terminal = ratatui::init();
let result = run(terminal);
ratatui::restore();
result
}
fn run(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
let mut app = App::default();
loop {
terminal.draw(|frame| app.render(frame))?;
if let Event::Key(key) = event::read()? {
if key.kind != KeyEventKind::Press {
continue;
}
match key.code {
KeyCode::Char('q') | KeyCode::Esc => break,
KeyCode::Char('j') | KeyCode::Down => app.next(),
KeyCode::Char('k') | KeyCode::Up => app.previous(),
KeyCode::Enter => app.select(),
_ => {}
}
}
}
Ok(())
}Layout System
Ratatui provides Layout to split areas into chunks using constraints.
use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
Frame,
};
fn render(frame: &mut Frame) {
let main_layout = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // header
Constraint::Min(0), // body
Constraint::Length(1), // footer
])
.split(frame.area());
let body_layout = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage(30), // sidebar
Constraint::Percentage(70), // content
])
.split(main_layout[1]);
render_header(frame, main_layout[0]);
render_sidebar(frame, body_layout[0]);
render_content(frame, body_layout[1]);
render_footer(frame, main_layout[2]);
}Constraint Types
| Constraint | Behavior |
|---|---|
Length(n) | Exact number of rows/columns |
Min(n) | At least n, takes remaining space |
Max(n) | At most n |
Percentage(n) | Percentage of available space |
Ratio(a, b) | Fraction a/b of available space |
Fill(weight) | Fills remaining space proportionally |
Widgets
Block and Paragraph
use ratatui::{
style::{Color, Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph, Wrap},
};
fn render_header(frame: &mut Frame, area: Rect) {
let title = Line::from(vec![
Span::styled("My App", Style::default().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
Span::raw(" v1.0"),
]);
let block = Block::default()
.title(title)
.borders(Borders::ALL)
.border_style(Style::default().fg(Color::Gray));
frame.render_widget(block, area);
}
fn render_content(frame: &mut Frame, area: Rect) {
let text = vec![
Line::from("First line of content"),
Line::from(vec![
Span::raw("Mixed "),
Span::styled("styled", Style::default().fg(Color::Yellow)),
Span::raw(" content"),
]),
];
let paragraph = Paragraph::new(text)
.block(Block::default().title("Content").borders(Borders::ALL))
.wrap(Wrap { trim: true });
frame.render_widget(paragraph, area);
}List
use ratatui::widgets::{List, ListItem, ListState, HighlightSpacing};
struct App {
items: Vec<String>,
state: ListState,
}
impl App {
fn render_list(&mut self, frame: &mut Frame, area: Rect) {
let items: Vec<ListItem> = self
.items
.iter()
.map(|i| ListItem::new(i.as_str()))
.collect();
let list = List::new(items)
.block(Block::default().title("Items").borders(Borders::ALL))
.highlight_style(Style::default().bg(Color::DarkGray).add_modifier(Modifier::BOLD))
.highlight_symbol("> ")
.highlight_spacing(HighlightSpacing::Always);
frame.render_stateful_widget(list, area, &mut self.state);
}
fn next(&mut self) {
let i = match self.state.selected() {
Some(i) if i >= self.items.len() - 1 => 0,
Some(i) => i + 1,
None => 0,
};
self.state.select(Some(i));
}
fn previous(&mut self) {
let i = match self.state.selected() {
Some(0) | None => self.items.len() - 1,
Some(i) => i - 1,
};
self.state.select(Some(i));
}
}Table
use ratatui::widgets::{Cell, Row, Table, TableState};
fn render_table(frame: &mut Frame, area: Rect, state: &mut TableState) {
let header = Row::new(vec![
Cell::from("Name").style(Style::default().fg(Color::Yellow)),
Cell::from("Email"),
Cell::from("Status"),
])
.height(1)
.bottom_margin(1);
let rows = vec![
Row::new(vec!["Alice", "alice@example.com", "Active"]),
Row::new(vec!["Bob", "bob@example.com", "Inactive"]),
];
let table = Table::new(rows, [
Constraint::Percentage(30),
Constraint::Percentage(50),
Constraint::Percentage(20),
])
.header(header)
.block(Block::default().title("Users").borders(Borders::ALL))
.highlight_style(Style::default().bg(Color::DarkGray));
frame.render_stateful_widget(table, area, state);
}Gauge and Sparkline
use ratatui::widgets::{Gauge, Sparkline};
fn render_progress(frame: &mut Frame, area: Rect, progress: f64) {
let gauge = Gauge::default()
.block(Block::default().title("Progress").borders(Borders::ALL))
.gauge_style(Style::default().fg(Color::Green))
.percent((progress * 100.0) as u16)
.label(format!("{:.1}%", progress * 100.0));
frame.render_widget(gauge, area);
}
fn render_sparkline(frame: &mut Frame, area: Rect, data: &[u64]) {
let sparkline = Sparkline::default()
.block(Block::default().title("Activity").borders(Borders::ALL))
.data(data)
.style(Style::default().fg(Color::Cyan));
frame.render_widget(sparkline, area);
}Event Handling with Polling
For non-blocking event handling, use event::poll with a timeout.
use std::time::{Duration, Instant};
use crossterm::event::{self, Event, KeyCode, KeyEventKind};
fn run_with_tick(mut terminal: DefaultTerminal) -> color_eyre::Result<()> {
let mut app = App::default();
let tick_rate = Duration::from_millis(250);
let mut last_tick = Instant::now();
loop {
terminal.draw(|frame| app.render(frame))?;
let timeout = tick_rate.saturating_sub(last_tick.elapsed());
if event::poll(timeout)? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
KeyCode::Char('q') => break,
_ => app.handle_key(key.code),
}
}
}
}
if last_tick.elapsed() >= tick_rate {
app.on_tick();
last_tick = Instant::now();
}
}
Ok(())
}Styling
use ratatui::style::{Color, Modifier, Style, Stylize};
let style = Style::default()
.fg(Color::Cyan)
.bg(Color::Black)
.add_modifier(Modifier::BOLD | Modifier::ITALIC);
// Shorthand with Stylize trait
let span = Span::raw("hello").cyan().bold();
let line = Line::from("status").green().on_dark_gray();Color Palette
| Color | Use |
|---|---|
Color::Reset | Terminal default |
Color::Rgb(r, g, b) | True color |
Color::Indexed(n) | 256-color palette |
Color::Red, Green, etc. | Basic 16 colors |
Release Pipeline
cargo-deny: Supply Chain Security
cargo-deny audits dependencies for license compliance, known vulnerabilities, yanked crates, and source restrictions.
cargo install cargo-deny
cargo deny init
cargo deny checkConfiguration
# deny.toml
[advisories]
vulnerability = "deny"
unmaintained = "warn"
yanked = "deny"
notice = "warn"
ignore = []
[licenses]
unlicensed = "deny"
allow = [
"MIT",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"ISC",
"Unicode-3.0",
]
copyleft = "deny"
[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [{ path = "LICENSE", hash = 0xbd0eed23 }]
[bans]
multiple-versions = "warn"
wildcards = "deny"
highlight = "all"
skip = []
# Deny specific crates
deny = [
{ name = "openssl" },
]
[sources]
unknown-registry = "deny"
unknown-git = "deny"
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
allow-git = []CI Integration
# .github/workflows/ci.yml
- name: Check dependencies
uses: EmbarkStudios/cargo-deny-action@v2
with:
command: check
arguments: --all-features# Check specific categories
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources
# Check all
cargo deny checkcargo-dist: Binary Distribution
cargo-dist automates building and distributing pre-built binaries across platforms, with support for installers and package manager taps.
cargo install cargo-dist
cargo dist initConfiguration
# Cargo.toml
[workspace.metadata.dist]
cargo-dist-version = "0.27.0"
ci = "github"
installers = ["shell", "powershell", "homebrew"]
targets = [
"aarch64-apple-darwin",
"x86_64-apple-darwin",
"x86_64-unknown-linux-gnu",
"aarch64-unknown-linux-gnu",
"x86_64-pc-windows-msvc",
]
tap = "myorg/homebrew-tap"
publish-jobs = ["homebrew"]
[workspace.metadata.dist.github-custom-runners]
aarch64-unknown-linux-gnu = "ubuntu-22.04-arm"Generated CI Workflow
Running cargo dist init generates a GitHub Actions workflow at .github/workflows/release.yml that builds binaries, creates GitHub releases, and publishes to configured installers when a tag is pushed.
# Preview what a release would produce
cargo dist plan
# Build for the current platform
cargo dist build
# Generate CI configuration
cargo dist generateInstaller Output
After release, users can install via:
# Shell installer (macOS/Linux)
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/org/repo/releases/latest/download/myapp-installer.sh | sh
# PowerShell installer (Windows)
powershell -ExecutionPolicy ByPass -c "irm https://github.com/org/repo/releases/latest/download/myapp-installer.ps1 | iex"
# Homebrew
brew install myorg/tap/myapprelease-plz: Automated Releases
release-plz automates changelog generation, version bumping based on conventional commits, and crate publishing.
cargo install release-plzConfiguration
# release-plz.toml
[workspace]
changelog_config = "cliff.toml"
publish = true
git_release_enable = true
git_tag_enable = true
[[package]]
name = "my-crate"
semver_check = true
[[package]]
name = "my-internal-crate"
publish = falseChangelog Configuration
# cliff.toml (git-cliff format)
[changelog]
header = "# Changelog\n\n"
body = """
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits %}
- {{ commit.message | upper_first }} ({{ commit.id | truncate(length=7, end="") }})\
{% endfor %}
{% endfor %}
"""
[git]
conventional_commits = true
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^doc", group = "Documentation" },
]CI Integration
# .github/workflows/release-plz.yml
name: Release-plz
on:
push:
branches: [main]
jobs:
release-plz:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: MarcoIeni/release-plz-action@v0.5
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }}Commands
# Preview what changes would be made
release-plz update --dry-run
# Update changelogs and bump versions
release-plz update
# Create a release PR
release-plz release-pr
# Publish to crates.io
release-plz releaseComplete CI Pipeline
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
env:
CARGO_TERM_COLOR: always
RUSTFLAGS: '-Dwarnings'
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
- name: Format
run: cargo fmt --all -- --check
- name: Clippy
run: cargo clippy --all-targets --all-features
- name: Test
run: cargo test --all-features
- name: Deny
uses: EmbarkStudios/cargo-deny-action@v2
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Install cargo-llvm-cov
uses: taiki-e/install-action@cargo-llvm-cov
- name: Coverage
run: cargo llvm-cov --all-features --lcov --output-path lcov.infoDocker Multi-Stage Build
FROM rust:1.83-slim AS builder
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --release
RUN rm -rf src
COPY src/ src/
RUN touch src/main.rs
RUN cargo build --release
FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/*
COPY --from=builder /app/target/release/myapp /usr/local/bin/
ENTRYPOINT ["myapp"]Testing and Benchmarks
Unit Tests
Unit tests live in the same file as the code they test, inside a #[cfg(test)] module.
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
pub fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
return Err("division by zero".into());
}
Ok(a / b)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_divide() {
let result = divide(10.0, 3.0).unwrap();
assert!((result - 3.333).abs() < 0.01);
}
#[test]
fn test_divide_by_zero() {
let result = divide(10.0, 0.0);
assert!(result.is_err());
assert_eq!(result.unwrap_err(), "division by zero");
}
#[test]
#[should_panic(expected = "index out of bounds")]
fn test_panics() {
let v: Vec<i32> = vec![];
let _ = v[0];
}
}Async Tests
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_async_fetch() {
let result = fetch_data("https://example.com").await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_with_timeout() {
let result = tokio::time::timeout(
std::time::Duration::from_secs(5),
slow_operation(),
)
.await;
assert!(result.is_ok());
}
}Integration Tests
Integration tests live in the tests/ directory at the project root. Each file is compiled as a separate crate.
project/
├── src/
│ └── lib.rs
├── tests/
│ ├── api_tests.rs
│ └── common/
│ └── mod.rs// tests/common/mod.rs
use myapp::AppState;
pub async fn setup() -> AppState {
let db = sqlx::PgPool::connect("postgres://localhost/test_db")
.await
.expect("failed to connect to test database");
sqlx::migrate!().run(&db).await.expect("migration failed");
AppState { db }
}
pub async fn cleanup(state: &AppState) {
sqlx::query("TRUNCATE users CASCADE")
.execute(&state.db)
.await
.expect("cleanup failed");
}// tests/api_tests.rs
mod common;
use axum::{body::Body, http::{Request, StatusCode}};
use tower::ServiceExt;
#[tokio::test]
async fn test_create_user() {
let state = common::setup().await;
let app = myapp::build_router(state.clone());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/api/v1/users")
.header("Content-Type", "application/json")
.body(Body::from(r#"{"name": "Alice", "email": "alice@test.com"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CREATED);
common::cleanup(&state).await;
}
#[tokio::test]
async fn test_get_nonexistent_user() {
let state = common::setup().await;
let app = myapp::build_router(state.clone());
let response = app
.oneshot(
Request::builder()
.uri("/api/v1/users/99999")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
common::cleanup(&state).await;
}Test Patterns
Test Fixtures with Builder
#[cfg(test)]
mod tests {
struct UserBuilder {
name: String,
email: String,
active: bool,
}
impl Default for UserBuilder {
fn default() -> Self {
Self {
name: "Test User".into(),
email: "test@example.com".into(),
active: true,
}
}
}
impl UserBuilder {
fn name(mut self, name: &str) -> Self {
self.name = name.into();
self
}
fn inactive(mut self) -> Self {
self.active = false;
self
}
fn build(self) -> User {
User {
id: 0,
name: self.name,
email: self.email,
active: self.active,
}
}
}
#[test]
fn test_inactive_user() {
let user = UserBuilder::default().inactive().build();
assert!(!user.active);
}
}Testing with Traits (Dependency Injection)
#[async_trait::async_trait]
pub trait UserRepository: Send + Sync {
async fn find(&self, id: i64) -> Result<Option<User>, anyhow::Error>;
async fn create(&self, name: &str, email: &str) -> Result<User, anyhow::Error>;
}
pub struct UserService<R: UserRepository> {
repo: R,
}
impl<R: UserRepository> UserService<R> {
pub fn new(repo: R) -> Self {
Self { repo }
}
pub async fn get_user(&self, id: i64) -> Result<User, anyhow::Error> {
self.repo
.find(id)
.await?
.ok_or_else(|| anyhow::anyhow!("user not found"))
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockRepo {
users: Vec<User>,
}
#[async_trait::async_trait]
impl UserRepository for MockRepo {
async fn find(&self, id: i64) -> Result<Option<User>, anyhow::Error> {
Ok(self.users.iter().find(|u| u.id == id).cloned())
}
async fn create(&self, name: &str, email: &str) -> Result<User, anyhow::Error> {
Ok(User { id: 1, name: name.into(), email: email.into(), active: true })
}
}
#[tokio::test]
async fn test_get_existing_user() {
let repo = MockRepo {
users: vec![User { id: 1, name: "Alice".into(), email: "a@b.com".into(), active: true }],
};
let service = UserService::new(repo);
let user = service.get_user(1).await.unwrap();
assert_eq!(user.name, "Alice");
}
#[tokio::test]
async fn test_get_missing_user() {
let repo = MockRepo { users: vec![] };
let service = UserService::new(repo);
let result = service.get_user(1).await;
assert!(result.is_err());
}
}Criterion Benchmarking
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
[[bench]]
name = "my_benchmark"
harness = false// benches/my_benchmark.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};
fn fibonacci(n: u64) -> u64 {
match n {
0 => 0,
1 => 1,
_ => fibonacci(n - 1) + fibonacci(n - 2),
}
}
fn bench_fibonacci(c: &mut Criterion) {
c.bench_function("fib 20", |b| b.iter(|| fibonacci(black_box(20))));
}
fn bench_fibonacci_group(c: &mut Criterion) {
let mut group = c.benchmark_group("fibonacci");
for size in [10, 15, 20, 25] {
group.bench_with_input(
BenchmarkId::from_parameter(size),
&size,
|b, &size| {
b.iter(|| fibonacci(black_box(size)));
},
);
}
group.finish();
}
fn bench_throughput(c: &mut Criterion) {
use criterion::Throughput;
let data: Vec<u8> = (0..1024).map(|i| (i % 256) as u8).collect();
let mut group = c.benchmark_group("processing");
group.throughput(Throughput::Bytes(data.len() as u64));
group.bench_function("process_data", |b| {
b.iter(|| process(black_box(&data)));
});
group.finish();
}
criterion_group!(benches, bench_fibonacci, bench_fibonacci_group, bench_throughput);
criterion_main!(benches);# Run benchmarks
cargo bench
# Run specific benchmark
cargo bench -- fibonacci
# HTML reports generated in target/criterion/Async Benchmarks with Criterion
use criterion::{criterion_group, criterion_main, Criterion};
use tokio::runtime::Runtime;
fn bench_async_operation(c: &mut Criterion) {
let rt = Runtime::new().unwrap();
c.bench_function("async fetch", |b| {
b.to_async(&rt).iter(|| async {
let _ = reqwest::get("http://localhost:3000/health").await;
});
});
}
criterion_group!(benches, bench_async_operation);
criterion_main!(benches);Cargo Test Commands
# Run all tests
cargo test
# Run tests with output
cargo test -- --nocapture
# Run specific test
cargo test test_name
# Run tests in specific module
cargo test module_name::
# Run only integration tests
cargo test --test api_tests
# Run tests with specific features
cargo test --features "feature_name"
# Run doc tests only
cargo test --doc