rust-ops
Rust development patterns, ownership, async, error handling, and ecosystem. Use for: rust, cargo, ownership, borrow checker, lifetime, tokio, serde, trait, Result, Option, async rust, crate, derive, impl, enum, pattern matching, Arc, Mutex, Send, Sync, thiserror, anyhow, clap, ax
Install
npx skills add https://github.com/0xDarkMatter/claude-mods/tree/main/skills/rust-ops
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install 0xdarkmatter-claude-mods@llmmart
git clone https://github.com/0xDarkMatter/claude-mods.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole 0xdarkmatter/claude-mods collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Rust Operations
Comprehensive Rust skill covering ownership, async, error handling, and the production ecosystem.
Ecosystem facts verified as of 2026-07.
Staleness check: python scripts/check-rust-facts.py --offline asserts the
catalogued version-bearing facts (tokio, axum, serde) are still named in the prose
and the dated currency note above is present; run --live to confirm each crate's
crates.io major still matches the documented major. Catalog: assets/rust-facts.json.
Ownership Quick Reference
Who owns the value?
│
├─ Need to transfer ownership
│ └─ Move: let s2 = s1; (s1 is invalid after this)
│
├─ Need to read without owning
│ └─ Shared borrow: &T (multiple allowed, no mutation)
│
├─ Need to mutate without owning
│ └─ Exclusive borrow: &mut T (only one, no other borrows)
│
├─ Need to share ownership across threads
│ └─ Arc<T> (atomic reference counting)
│ └─ Need mutation too? Arc<Mutex<T>>
│
├─ Need to share ownership single-threaded
│ └─ Rc<T> (reference counting, not Send)
│ └─ Need mutation too? Rc<RefCell<T>>
│
└─ Need to avoid cloning large data
└─ Cow<'a, T> (clone-on-write, borrows when possible)
The Borrow Rules
- At any time, you can have either one
&mut Tor any number of&T - References must always be valid (no dangling)
- These rules are enforced at compile time (zero runtime cost)
Error Handling Decision Tree
What kind of error?
│
├─ Operation might not have a value (no error info needed)
│ └─ Option<T>: Some(value) or None
│
├─ Library code (callers need to match on error variants)
│ └─ thiserror: #[derive(Error)] enum with variants
│ └─ Each variant can wrap source errors with #[from]
│
├─ Application code (just need context, not matching)
│ └─ anyhow: anyhow::Result<T>, .context("msg")
│
├─ Converting between error types
│ └─ impl From<SourceError> for MyError
│ └─ Or use #[from] with thiserror
│
└─ Truly unrecoverable (violating invariants)
└─ panic!() or unwrap() - avoid in library code
thiserror (Library Errors)
use thiserror::Error;
#[derive(Debug, Error)]
pub enum AppError {
#[error("database error: {0}")]
Database(#[from] sqlx::Error),
#[error("not found: {entity} with id {id}")]
NotFound { entity: &'static str, id: i64 },
#[error("validation failed: {0}")]
Validation(String),
}
anyhow (Application Errors)
use anyhow::{Context, Result};
fn load_config(path: &str) -> Result<Config> {
let content = std::fs::read_to_string(path)
.context("failed to read config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse config")?;
Ok(config)
}
The ? Operator
// ? on Result: returns Err early, unwraps Ok
let file = File::open(path)?;
// ? on Option: returns None early, unwraps Some
let first = items.first()?;
// Chain with map_err for context
let port: u16 = env::var("PORT")
.map_err(|_| AppError::Config("PORT not set"))?
.parse()
.map_err(|_| AppError::Config("PORT not a number"))?;
Deep dive: Load ./references/error-handling.md for Result/Option combinators, error conversion patterns, panic/recover.
Trait Design Quick Reference
Common Derives
#[derive(Debug, Clone, PartialEq, Eq, Hash)] // Value types
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] // API types
#[derive(Debug, thiserror::Error)] // Error types
Trait Objects vs Generics
Trait Objects (dyn Trait) |
Generics (T: Trait) |
|
|---|---|---|
| Dispatch | Dynamic (vtable) | Static (monomorphized) |
| Binary size | Smaller | Larger (per-type copies) |
| Performance | Slight overhead | Zero-cost |
| Heterogeneous collections | Yes | No |
| Use when | Runtime polymorphism, plugin systems | Performance-critical, known types |
// Generics (preferred when types known at compile time)
fn process<T: Display>(item: T) { println!("{item}"); }
// Trait objects (when you need heterogeneous collections)
fn process_all(items: &[Box<dyn Display>]) {
for item in items { println!("{item}"); }
}
Key Traits to Know
| Trait | Purpose | Auto-derive? |
|---|---|---|
Debug |
Debug formatting | Yes |
Clone |
Explicit copy | Yes |
Copy |
Implicit copy (small, stack-only) | Yes |
Display |
User-facing formatting | No (impl manually) |
From/Into |
Type conversion | No (impl From, get Into free) |
Send |
Safe to send between threads | Auto |
Sync |
Safe to share references between threads | Auto |
Deref |
Smart pointer dereference | No |
Iterator |
Iteration protocol | No |
Default |
Default value | Yes |
Deep dive: Load ./references/traits-generics.md for associated types, supertraits, sealed traits, extension traits.
Async Decision Tree
Do you need async?
│
├─ I/O-heavy (network, files, databases)
│ └─ Yes. Use tokio.
│
├─ CPU-heavy computation
│ └─ No. Use rayon for data parallelism.
│ └─ Or tokio::task::spawn_blocking for mixing with async
│
├─ Simple scripts or CLI tools
│ └─ Probably not. Blocking I/O is fine.
│
└─ Yes, I need async:
│
├─ Runtime: tokio (dominant), or async-std
├─ HTTP client: reqwest
├─ HTTP server: axum (tower-based) or actix-web
├─ Database: sqlx (compile-time checked)
└─ Structured logging: tracing
tokio Quick Start
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Spawn concurrent tasks
let (a, b) = tokio::join!(
fetch_users(),
fetch_orders(),
);
// Select first to complete
tokio::select! {
result = long_operation() => handle(result),
_ = tokio::time::sleep(Duration::from_secs(5)) => {
eprintln!("timeout");
}
}
Ok(())
}
Channel Types
| Channel | Use Case | Import |
|---|---|---|
mpsc |
Multiple producers, single consumer | tokio::sync::mpsc |
oneshot |
Single value, single use | tokio::sync::oneshot |
broadcast |
Multiple consumers, all get every message | tokio::sync::broadcast |
watch |
Single value, latest-only (config reload) | tokio::sync::watch |
Deep dive: Load ./references/async-tokio.md for spawn patterns, graceful shutdown, Mutex choice, async traits, streams.
Cargo Quick Reference
# Create project
cargo new my-project # binary
cargo new my-lib --lib # library
# Build and run
cargo build # debug
cargo build --release # optimized
cargo run -- args # build + run
cargo run --example name # run example
# Test
cargo test # all tests
cargo test test_name # specific test
cargo test -- --nocapture # show println output
# Dependencies
cargo add serde --features derive # add dep
cargo add tokio -F full # shorthand
cargo update # update lock file
# Check without building
cargo check # fast type checking
cargo clippy # lints
cargo fmt # format
# Workspace
cargo test --workspace # test all crates
cargo build -p my-crate # build specific crate
Feature Flags
[features]
default = ["json"]
json = ["dep:serde_json"]
full = ["json", "yaml", "toml"]
[dependencies]
serde_json = { version = "1", optional = true }
Release Profile Tuning
[profile.release]
lto = true # Link-time optimization: smaller, faster binaries
codegen-units = 1 # Better optimization at the cost of compile time
Common Gotchas
| Gotcha | Why | Fix |
|---|---|---|
String vs &str |
Owned vs borrowed, function signatures | Accept &str in params, return String |
| Borrow checker fight | Borrowing self while mutating | Split struct, use indices, clone (if cheap) |
| Lifetime elision confusion | Hidden lifetimes in function signatures | Write them out explicitly to understand, then elide |
impl Trait in return |
Different branches must return same type | Use Box<dyn Trait> for heterogeneous returns |
tokio::Mutex vs std::Mutex |
std::Mutex can't be held across .await |
Use tokio::Mutex across await points |
| Orphan rule | Can't impl foreign trait for foreign type | Newtype pattern: struct Wrapper(ForeignType) |
Pin confusion |
Required for self-referential async futures | Use Box::pin(), don't fight it |
Send bounds on async |
Spawned futures must be Send |
Avoid Rc, RefCell in async; use Arc, Mutex |
.unwrap() in production |
Panics on None/Err | Use ?, .unwrap_or(), .expect("reason") |
serde Quick Reference
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
struct User {
user_id: i64,
display_name: String,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(default)]
is_active: bool,
#[serde(rename = "type")]
user_type: UserType,
#[serde(with = "chrono::serde::ts_seconds")]
created_at: DateTime<Utc>,
}
// Serialize
let json = serde_json::to_string(&user)?;
let yaml = serde_yaml::to_string(&user)?;
// Deserialize
let user: User = serde_json::from_str(&json)?;
Deep dive: Load ./references/ecosystem.md for serde advanced usage, clap, reqwest, sqlx, axum, tracing, rayon.
Reference Files
Load these for deep-dive topics. Each is self-contained.
| Reference | When to Load |
|---|---|
./references/ownership-lifetimes.md |
Borrowing rules, lifetime annotations, elision, interior mutability, common borrow checker patterns |
./references/traits-generics.md |
Trait design, associated types, supertraits, generics, constraints, sealed/extension traits |
./references/error-handling.md |
Result/Option combinators, thiserror/anyhow deep dive, error conversion, panic/recover |
./references/async-tokio.md |
tokio runtime, spawn, channels, select, streams, graceful shutdown, async traits, Mutex choice |
./references/ecosystem.md |
serde advanced, clap, reqwest, sqlx, axum, tracing, rayon, itertools, Cow |
./references/testing.md |
Unit/integration/doc tests, async tests, mockall, proptest, criterion benchmarks |
See Also
docker-ops- Multi-stage builds for Rust (scratch/distroless, cargo-chef for layer caching)ci-cd-ops- Rust CI pipelines, cargo caching, cross-compilationtesting-ops- Cross-language testing strategies
Files (claude-mods)
-
assets
-
rust-facts.json 1.3 KB
{ "_comment": "Canonical fast-moving facts the rust-ops skill encodes. scripts/check-rust-facts.py asserts SKILL.md + references name these consistently (--offline) and probes crates.io for major-version drift (--live). Edit deliberately: a change here is a skill-content decision, not housekeeping.", "schema": "claude-mods.rust-ops.facts/v1", "as_of": "2026-07-05", "registry": "https://crates.io/api/v1/crates", "currency_note": "> Ecosystem facts verified as of 2026-07.", "packages": [ { "name": "tokio", "documented_major": 1, "role": "async runtime (dominant)", "where": "SKILL.md (#[tokio::main], tokio::join!/select!, mpsc/oneshot/broadcast/watch channels); references/async-tokio.md. The #[tokio::main]/#[tokio::test] + tokio::join! surface is the Tokio 1.x API." }, { "name": "axum", "documented_major": 0, "role": "HTTP server (tower-based)", "where": "SKILL.md (async decision tree: 'axum (tower-based)'); references/ecosystem.md (axum::Router, axum::serve, extractors). Axum has shipped as 0.x throughout." }, { "name": "serde", "documented_major": 1, "role": "serialization", "where": "SKILL.md (#[derive(Serialize, Deserialize)], serde_json::to_string); references/ecosystem.md. The derive + serde_json surface is the Serde 1.x API." } ] }
-
-
references
-
async-tokio.md 22.4 KB
# Rust Async and Tokio Reference ## Table of Contents 1. [tokio Runtime](#1-tokio-runtime) 2. [Spawn Tasks](#2-spawn-tasks) 3. [Select](#3-select) 4. [Channels](#4-channels) 5. [Async Streams](#5-async-streams) 6. [Timeouts and Sleep](#6-timeouts-and-sleep) 7. [Async Traits](#7-async-traits) 8. [Mutex Choice](#8-mutex-choice) 9. [Graceful Shutdown](#9-graceful-shutdown) 10. [Connection Pooling](#10-connection-pooling) 11. [Test Async Code](#11-test-async-code) 12. [Common Async Mistakes](#12-common-async-mistakes) --- ## 1. tokio Runtime ### #[tokio::main] ```rust // Multi-threaded (default): uses all CPU cores #[tokio::main] async fn main() -> anyhow::Result<()> { run().await } // Single-threaded: useful for tests or embedded contexts #[tokio::main(flavor = "current_thread")] async fn main() -> anyhow::Result<()> { run().await } // With worker count #[tokio::main(worker_threads = 4)] async fn main() -> anyhow::Result<()> { run().await } ``` ### runtime::Builder ```rust use tokio::runtime::Builder; fn main() -> anyhow::Result<()> { let rt = Builder::new_multi_thread() .worker_threads(4) .thread_name("my-worker") .thread_stack_size(3 * 1024 * 1024) .enable_all() // enables both io and time drivers .build()?; rt.block_on(async { run().await }) } // current_thread runtime for single-threaded executors let rt = Builder::new_current_thread() .enable_all() .build()?; ``` ### runtime::Handle ```rust use tokio::runtime::Handle; // Obtain a handle from within an async context let handle = Handle::current(); // Spawn onto the runtime from a sync context std::thread::spawn(move || { handle.spawn(async { do_work().await }); handle.block_on(async { do_sync_work().await }); }); // try_current(): returns None outside of a runtime if let Ok(handle) = Handle::try_current() { handle.spawn(async { background_task().await }); } ``` --- ## 2. Spawn Tasks ### tokio::spawn and JoinHandle ```rust use tokio::task::JoinHandle; async fn run() { // spawn returns JoinHandle<T> let handle: JoinHandle<u32> = tokio::spawn(async { compute().await }); // await the result — JoinHandle<T> returns Result<T, JoinError> match handle.await { Ok(value) => println!("got {value}"), Err(e) if e.is_panic() => eprintln!("task panicked"), Err(e) => eprintln!("task cancelled: {e}"), } } ``` ### JoinSet for Multiple Tasks ```rust use tokio::task::JoinSet; async fn fetch_all(urls: Vec<String>) -> Vec<Result<String, reqwest::Error>> { let mut set = JoinSet::new(); for url in urls { set.spawn(async move { reqwest::get(&url).await?.text().await }); } let mut results = Vec::new(); while let Some(res) = set.join_next().await { results.push(res.expect("task panicked")); } results } // Abort all remaining tasks when JoinSet drops (or explicitly) set.abort_all(); ``` ### abort ```rust let handle = tokio::spawn(async { loop { do_work().await; } }); // Cancel the task handle.abort(); // abort() is best-effort: the task must be at an await point // Check completion after abort match handle.await { Err(e) if e.is_cancelled() => println!("task was cancelled"), _ => {} } ``` ### spawn_blocking for CPU Work ```rust // Never block the async executor — offload CPU work to a thread pool async fn hash_password(password: String) -> String { tokio::task::spawn_blocking(move || { bcrypt::hash(&password, 12).unwrap() }) .await .expect("blocking task panicked") } // spawn_blocking has a default limit of 512 threads // For unbounded work, consider a dedicated rayon thread pool async fn process_image(data: Vec<u8>) -> Vec<u8> { tokio::task::spawn_blocking(move || { rayon_heavy_transform(data) }) .await .unwrap() } ``` --- ## 3. Select ### tokio::select! Basics ```rust use tokio::select; async fn race() -> &'static str { select! { result = task_a() => { println!("A won: {result:?}"); "a" } result = task_b() => { println!("B won: {result:?}"); "b" } } // Unselected branch future is dropped immediately } ``` ### biased for Deterministic Priority ```rust select! { biased; // arms checked top-to-bottom, not randomly // shutdown takes priority over incoming work _ = shutdown_signal() => { cleanup().await; } msg = receiver.recv() => { process(msg).await; } } ``` ### Cancellation Safety ```rust // Only use futures that are cancellation-safe in select! // Safe: recv(), accept(), sleep(), read_line() // NOT safe: read_to_end(), write_all() (partial progress is lost) // For non-cancellation-safe futures, pin them and reuse let mut read_future = Box::pin(file.read_to_end(&mut buf)); loop { select! { result = &mut read_future => { // Only enters here when done, previous progress preserved break result; } _ = shutdown.recv() => { break Err(io::Error::new(io::ErrorKind::Interrupted, "shutdown")); } } } ``` ### loop + select Pattern ```rust async fn event_loop( mut rx: mpsc::Receiver<Message>, mut shutdown: broadcast::Receiver<()>, ) { loop { select! { Some(msg) = rx.recv() => { handle_message(msg).await; } _ = shutdown.recv() => { tracing::info!("shutting down event loop"); break; } else => { // All branches are disabled (channels closed) break; } } } } ``` --- ## 4. Channels ### mpsc — Multi-Producer, Single-Consumer ```rust use tokio::sync::mpsc; // Bounded channel (backpressure built-in) let (tx, mut rx) = mpsc::channel::<String>(32); // Clone the sender for multiple producers let tx2 = tx.clone(); tokio::spawn(async move { tx.send("hello".to_string()).await.unwrap(); }); tokio::spawn(async move { tx2.send("world".to_string()).await.unwrap(); }); while let Some(msg) = rx.recv().await { println!("received: {msg}"); } // recv() returns None when all senders are dropped // Unbounded channel (no backpressure — use carefully) let (tx, mut rx) = mpsc::unbounded_channel::<String>(); tx.send("hello".to_string()).unwrap(); // non-async send ``` ### oneshot — Single Value ```rust use tokio::sync::oneshot; // One sender, one receiver, one value let (tx, rx) = oneshot::channel::<u64>(); tokio::spawn(async move { let result = compute().await; tx.send(result).ok(); // ok() because receiver might have dropped }); match rx.await { Ok(value) => println!("computed: {value}"), Err(_) => println!("sender dropped before sending"), } // Common pattern: request-response over a channel struct Request { data: Vec<u8>, reply: oneshot::Sender<Result<Vec<u8>, Error>>, } ``` ### broadcast — Single-Producer, Multi-Consumer ```rust use tokio::sync::broadcast; // All active receivers get every message let (tx, mut rx1) = broadcast::channel::<String>(16); let mut rx2 = tx.subscribe(); tx.send("announcement".to_string()).unwrap(); // Each receiver gets its own copy let msg1 = rx1.recv().await.unwrap(); let msg2 = rx2.recv().await.unwrap(); // Lagged receiver: if receiver falls behind capacity, it gets Err(Lagged(n)) match rx1.recv().await { Ok(msg) => handle(msg), Err(broadcast::error::RecvError::Lagged(n)) => { tracing::warn!("missed {n} messages, resyncing"); } Err(broadcast::error::RecvError::Closed) => break, } ``` ### watch — Single Writer, Multi-Reader (Latest Value) ```rust use tokio::sync::watch; // Only the most recent value is retained let (tx, rx) = watch::channel(Config::default()); // Writer updates the shared value tokio::spawn(async move { loop { let new_config = reload_config().await; tx.send(new_config).unwrap(); tokio::time::sleep(Duration::from_secs(30)).await; } }); // Readers clone a receiver and watch for changes tokio::spawn(async move { let mut rx = rx; loop { rx.changed().await.unwrap(); // waits for a new value let config = rx.borrow_and_update().clone(); apply_config(config).await; } }); ``` --- ## 5. Async Streams ### Stream Trait and StreamExt ```rust use futures::StreamExt; // or tokio_stream::StreamExt // Streams are async iterators async fn process_stream<S>(mut stream: S) where S: futures::Stream<Item = Event> + Unpin, { while let Some(event) = stream.next().await { handle(event).await; } // StreamExt combinators stream.map(|e| transform(e)) .filter(|e| futures::future::ready(e.important)) .take(100) .for_each(|e| async move { handle(e).await }) .await; } ``` ### tokio_stream ```rust use tokio_stream::{self as stream, StreamExt}; // Stream from iterator let s = stream::iter(vec![1, 2, 3]); // Stream with delay between items let s = stream::iter(vec![1, 2, 3]) .throttle(Duration::from_millis(100)); // Merge streams let s = stream::select(stream_a, stream_b); // Wrap a channel receiver as a stream let stream = tokio_stream::wrappers::ReceiverStream::new(rx); let stream = tokio_stream::wrappers::BroadcastStream::new(rx); let stream = tokio_stream::wrappers::WatchStream::new(rx); ``` ### Create Streams with async_stream ```rust use async_stream::stream; fn paginate(client: Client, query: Query) -> impl futures::Stream<Item = Record> { stream! { let mut cursor = None; loop { let page = client.fetch(query.clone(), cursor).await.unwrap(); for record in page.records { yield record; } match page.next_cursor { Some(c) => cursor = Some(c), None => break, } } } } ``` --- ## 6. Timeouts and Sleep ### tokio::time::sleep ```rust use tokio::time::{sleep, Duration}; // Non-blocking sleep sleep(Duration::from_secs(1)).await; // Sleep until a specific instant use tokio::time::Instant; sleep(Instant::now() + Duration::from_millis(500) - Instant::now()).await; ``` ### timeout ```rust use tokio::time::timeout; match timeout(Duration::from_secs(5), fetch_data()).await { Ok(Ok(data)) => process(data), Ok(Err(e)) => handle_error(e), Err(_elapsed) => eprintln!("request timed out"), } // timeout returns Err(Elapsed) on timeout // The inner future is cancelled when timeout fires ``` ### interval ```rust use tokio::time::{interval, MissedTickBehavior}; async fn heartbeat() { let mut ticker = interval(Duration::from_secs(10)); ticker.set_missed_tick_behavior(MissedTickBehavior::Skip); loop { ticker.tick().await; send_heartbeat().await; } } // MissedTickBehavior options: // Burst (default): catch up all missed ticks immediately // Skip: skip missed ticks, tick at next aligned interval // Delay: delay next tick by full interval from now ``` ### Instant ```rust use tokio::time::Instant; let start = Instant::now(); do_work().await; let elapsed = start.elapsed(); tracing::info!(?elapsed, "work completed"); ``` --- ## 7. Async Traits ### RPITIT (Rust 1.75+, Preferred) ```rust // Async functions in traits work natively since Rust 1.75 pub trait DataStore { async fn get(&self, key: &str) -> Option<String>; async fn set(&self, key: &str, value: String) -> Result<(), Error>; } impl DataStore for RedisStore { async fn get(&self, key: &str) -> Option<String> { self.client.get(key).await.ok() } async fn set(&self, key: &str, value: String) -> Result<(), Error> { self.client.set(key, value).await?; Ok(()) } } ``` ### trait_variant for dyn Compatibility ```rust // Native async traits are not dyn-safe by default // Use trait_variant for trait objects use trait_variant::make; #[make(SendDataStore: Send)] pub trait DataStore { async fn get(&self, key: &str) -> Option<String>; } // Now use dyn SendDataStore for boxed trait objects fn make_store() -> Box<dyn SendDataStore> { Box::new(RedisStore::new()) } ``` ### async-trait Crate (Pre-1.75 or dyn-safe) ```rust use async_trait::async_trait; #[async_trait] pub trait Handler: Send + Sync { async fn handle(&self, req: Request) -> Response; } #[async_trait] impl Handler for MyHandler { async fn handle(&self, req: Request) -> Response { process(req).await } } // async_trait boxes the returned future automatically // Zero-cost in practice but adds a heap allocation per call ``` ### Manual Approach (Maximum Control) ```rust use std::future::Future; use std::pin::Pin; pub trait Handler: Send + Sync { fn handle<'a>( &'a self, req: Request, ) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>>; } impl Handler for MyHandler { fn handle<'a>(&'a self, req: Request) -> Pin<Box<dyn Future<Output = Response> + Send + 'a>> { Box::pin(async move { process(&self.state, req).await }) } } ``` --- ## 8. Mutex Choice ### tokio::sync::Mutex vs std::sync::Mutex ```rust // Use std::sync::Mutex when: // - Lock is held only for synchronous operations (no .await inside lock) // - Lock contention is low // - You want lower overhead use std::sync::Mutex; struct Cache { inner: Mutex<HashMap<String, String>>, } impl Cache { fn get(&self, key: &str) -> Option<String> { self.inner.lock().unwrap().get(key).cloned() } async fn get_or_fetch(&self, key: &str) -> String { if let Some(v) = self.get(key) { return v; } let value = fetch(key).await; // lock NOT held during await self.inner.lock().unwrap().insert(key.to_string(), value.clone()); value } } ``` ```rust // Use tokio::sync::Mutex when: // - You need to hold the lock across .await points // - Multiple async tasks contend and fairness matters use tokio::sync::Mutex; struct Connection { inner: Mutex<TcpStream>, } impl Connection { async fn send_and_receive(&self, data: &[u8]) -> Vec<u8> { let mut conn = self.inner.lock().await; // lock held across awaits conn.write_all(data).await.unwrap(); let mut buf = vec![0u8; 1024]; conn.read(&mut buf).await.unwrap(); buf } } ``` ### RwLock ```rust use tokio::sync::RwLock; struct Config { data: RwLock<ConfigData>, } impl Config { async fn get(&self) -> ConfigData { self.data.read().await.clone() // multiple concurrent readers } async fn update(&self, new: ConfigData) { *self.data.write().await = new; // exclusive writer } } // RwLock can starve writers if readers are constant — monitor in practice ``` --- ## 9. Graceful Shutdown ### Signal Handling ```rust use tokio::signal; async fn wait_for_shutdown() { let ctrl_c = async { signal::ctrl_c().await.expect("failed to install Ctrl+C handler"); }; #[cfg(unix)] let sigterm = async { signal::unix::signal(signal::unix::SignalKind::terminate()) .expect("failed to install SIGTERM handler") .recv() .await; }; #[cfg(not(unix))] let sigterm = std::future::pending::<()>(); tokio::select! { _ = ctrl_c => {}, _ = sigterm => {}, } tracing::info!("shutdown signal received"); } ``` ### CancellationToken ```rust use tokio_util::sync::CancellationToken; #[tokio::main] async fn main() { let token = CancellationToken::new(); // Give child tasks a clone let worker_token = token.child_token(); tokio::spawn(async move { select! { _ = worker_token.cancelled() => { tracing::info!("worker shutting down"); } _ = do_work() => {} } }); // Trigger shutdown on signal wait_for_shutdown().await; token.cancel(); // Give tasks time to drain tokio::time::sleep(Duration::from_secs(5)).await; } ``` ### Draining Connections and Shutdown Sequence ```rust async fn shutdown( server: Server, mut rx: mpsc::Receiver<()>, token: CancellationToken, ) { // 1. Stop accepting new connections server.stop_accepting(); // 2. Signal all workers token.cancel(); // 3. Wait for in-flight requests (with timeout) let drain = timeout(Duration::from_secs(30), server.drain()); match drain.await { Ok(_) => tracing::info!("clean shutdown"), Err(_) => tracing::warn!("shutdown timeout: forcing exit"), } // 4. Flush telemetry, close DB pools, etc. flush_telemetry().await; } ``` --- ## 10. Connection Pooling ### sqlx Pool ```rust use sqlx::postgres::PgPoolOptions; let pool = PgPoolOptions::new() .max_connections(20) .min_connections(2) .acquire_timeout(Duration::from_secs(5)) .idle_timeout(Duration::from_secs(600)) .connect("postgres://user:pass@localhost/db") .await?; // Clone the pool cheaply — it's Arc internally async fn get_user(pool: &sqlx::PgPool, id: i64) -> sqlx::Result<User> { sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", id) .fetch_one(pool) .await } ``` ### reqwest Client Reuse ```rust use reqwest::Client; // Build once, clone cheaply (Arc internally) let client = Client::builder() .timeout(Duration::from_secs(30)) .pool_max_idle_per_host(10) .connection_verbose(false) .build()?; // Share via state, not by creating new clients per request #[derive(Clone)] struct AppState { http: Client, db: sqlx::PgPool, } ``` ### bb8 Generic Pool ```rust use bb8::Pool; use bb8_redis::RedisConnectionManager; let manager = RedisConnectionManager::new("redis://localhost")?; let pool = Pool::builder() .max_size(15) .min_idle(Some(2)) .connection_timeout(Duration::from_secs(3)) .build(manager) .await?; let mut conn = pool.get().await?; redis::cmd("SET").arg("key").arg("value").query_async(&mut *conn).await?; ``` --- ## 11. Test Async Code ### #[tokio::test] ```rust #[tokio::test] async fn test_fetch_user() { let db = setup_test_db().await; let user = db.get_user(1).await.unwrap(); assert_eq!(user.name, "Alice"); } // Multi-thread flavor for concurrency tests #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn test_concurrent_writes() { let state = Arc::new(SharedState::new()); let handles: Vec<_> = (0..10) .map(|i| { let s = state.clone(); tokio::spawn(async move { s.write(i).await }) }) .collect(); futures::future::join_all(handles).await; assert_eq!(state.count().await, 10); } ``` ### Mock Time with tokio::time::pause ```rust #[tokio::test] async fn test_timeout_behavior() { tokio::time::pause(); // freeze time let result = tokio::spawn(async { timeout(Duration::from_secs(5), slow_operation()).await }); // Advance time without actually waiting tokio::time::advance(Duration::from_secs(6)).await; let outcome = result.await.unwrap(); assert!(outcome.is_err(), "should have timed out"); } ``` ### Testing Channels ```rust #[tokio::test] async fn test_message_processing() { let (tx, rx) = mpsc::channel(8); let processor = Processor::new(rx); let handle = tokio::spawn(processor.run()); tx.send(Message::Ping).await.unwrap(); tx.send(Message::Stop).await.unwrap(); drop(tx); handle.await.unwrap(); } // Test that a task completes within expected time #[tokio::test] async fn test_completes_quickly() { let result = timeout(Duration::from_millis(100), fast_task()).await; assert!(result.is_ok(), "task took too long"); } ``` --- ## 12. Common Async Mistakes ### Block in Async Context ```rust // BAD: blocks the executor thread, starves other tasks async fn hash(password: &str) -> String { bcrypt::hash(password, 12).unwrap() // CPU-intensive, blocks } // GOOD: offload to blocking thread pool async fn hash(password: String) -> String { tokio::task::spawn_blocking(move || { bcrypt::hash(&password, 12).unwrap() }) .await .unwrap() } // BAD: blocking sleep async fn wait() { std::thread::sleep(Duration::from_secs(1)); // blocks executor } // GOOD: async fn wait() { tokio::time::sleep(Duration::from_secs(1)).await; } ``` ### Hold std::Mutex Across .await ```rust // BAD: MutexGuard (which is not Send) held across await point // This will fail to compile if the future must be Send async fn bad_update(state: Arc<Mutex<State>>) { let mut guard = state.lock().unwrap(); guard.count += 1; do_async_work().await; // guard still held — not Send! guard.finalize(); } // GOOD: release lock before await async fn good_update(state: Arc<Mutex<State>>) { { let mut guard = state.lock().unwrap(); guard.count += 1; } // guard dropped here do_async_work().await; { let mut guard = state.lock().unwrap(); guard.finalize(); } } // ALTERNATIVE: use tokio::sync::Mutex if you need to hold across awaits ``` ### Missing Send Bounds ```rust // Task spawned with tokio::spawn must be Send + 'static // This fails if you capture non-Send types (like Rc, RefCell) let rc = Rc::new(5); tokio::spawn(async move { println!("{}", rc); // ERROR: Rc is not Send }); // GOOD: use Arc instead of Rc let arc = Arc::new(5); tokio::spawn(async move { println!("{}", arc); // OK }); ``` ### Forget to Drive Futures ```rust // BAD: creating a future without awaiting it — nothing happens async fn fire_and_maybe_forget() { let future = send_email("user@example.com"); // not awaited, not spawned // future is dropped, email never sent } // GOOD: either await or spawn async fn fire_and_actually_do_it() { // Option 1: await (sequential) send_email("user@example.com").await.unwrap(); // Option 2: spawn (concurrent, detached) tokio::spawn(async { send_email("user@example.com").await.ok() }); } ``` ### Unbounded Spawning ```rust // BAD: spawning one task per item with no limit — OOM on large input async fn process_all(items: Vec<Item>) { for item in items { tokio::spawn(async move { process(item).await }); } } // GOOD: use JoinSet with capacity limit or a semaphore use tokio::sync::Semaphore; async fn process_all(items: Vec<Item>) { let sem = Arc::new(Semaphore::new(50)); // max 50 concurrent tasks let mut set = JoinSet::new(); for item in items { let permit = sem.clone().acquire_owned().await.unwrap(); set.spawn(async move { let _permit = permit; // released when task ends process(item).await }); } while set.join_next().await.is_some() {} } ``` -
ecosystem.md 22.7 KB
# Rust Ecosystem Reference ## Table of Contents 1. [serde Advanced](#1-serde-advanced) 2. [clap](#2-clap) 3. [reqwest](#3-reqwest) 4. [sqlx](#4-sqlx) 5. [axum](#5-axum) 6. [tracing](#6-tracing) 7. [rayon](#7-rayon) 8. [itertools](#8-itertools) 9. [Cow](#9-cow) --- ## 1. serde Advanced ### Use Custom Serialization with `serialize_with` / `deserialize_with` ```rust use serde::{Deserialize, Serialize}; use chrono::{DateTime, Utc}; #[derive(Serialize, Deserialize)] pub struct Event { pub name: String, #[serde(with = "chrono::serde::ts_seconds")] pub occurred_at: DateTime<Utc>, #[serde( serialize_with = "serialize_uppercase", deserialize_with = "deserialize_uppercase" )] pub code: String, } fn serialize_uppercase<S>(value: &str, s: S) -> Result<S::Ok, S::Error> where S: serde::Serializer, { s.serialize_str(&value.to_uppercase()) } fn deserialize_uppercase<'de, D>(d: D) -> Result<String, D::Error> where D: serde::Deserializer<'de>, { let raw = String::deserialize(d)?; Ok(raw.to_uppercase()) } ``` ### Use `#[serde(with)]` for Custom Module ```rust mod as_base64 { use base64::{engine::general_purpose, Engine}; use serde::{Deserialize, Deserializer, Serializer}; pub fn serialize<S>(bytes: &[u8], s: S) -> Result<S::Ok, S::Error> where S: Serializer, { s.serialize_str(&general_purpose::STANDARD.encode(bytes)) } pub fn deserialize<'de, D>(d: D) -> Result<Vec<u8>, D::Error> where D: Deserializer<'de>, { let s = String::deserialize(d)?; general_purpose::STANDARD .decode(&s) .map_err(serde::de::Error::custom) } } #[derive(Serialize, Deserialize)] pub struct Secret { #[serde(with = "as_base64")] pub key: Vec<u8>, } ``` ### Flatten Nested Structs ```rust #[derive(Serialize, Deserialize)] pub struct Metadata { pub created_by: String, pub version: u32, } #[derive(Serialize, Deserialize)] pub struct Record { pub id: u64, pub name: String, #[serde(flatten)] pub meta: Metadata, // Serializes as: { "id": 1, "name": "...", "created_by": "...", "version": 1 } } // Capture unknown fields #[derive(Serialize, Deserialize)] pub struct Flexible { pub known: String, #[serde(flatten)] pub extra: std::collections::HashMap<String, serde_json::Value>, } ``` ### Tag Enums (Internal, External, Adjacent, Untagged) ```rust // External (default): { "TypeName": { ...fields } } #[derive(Serialize, Deserialize)] pub enum External { Text { content: String }, Number { value: i64 }, } // Internal: { "type": "Text", "content": "..." } #[derive(Serialize, Deserialize)] #[serde(tag = "type")] pub enum Internal { Text { content: String }, Number { value: i64 }, } // Adjacent: { "type": "Text", "data": { "content": "..." } } #[derive(Serialize, Deserialize)] #[serde(tag = "type", content = "data")] pub enum Adjacent { Text { content: String }, Number { value: i64 }, } // Untagged: tries each variant until one succeeds #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum Untagged { Text { content: String }, Number { value: i64 }, Raw(String), } ``` ### Reject Unknown Fields ```rust #[derive(Deserialize)] #[serde(deny_unknown_fields)] pub struct StrictConfig { pub host: String, pub port: u16, // Any unknown key in JSON causes deserialization to fail } ``` ### Use `#[serde(remote)]` for External Types ```rust // For types you don't own, create a remote definition #[derive(Serialize, Deserialize)] #[serde(remote = "std::time::Duration")] struct DurationDef { secs: u64, nanos: u32, } #[derive(Serialize, Deserialize)] pub struct Config { #[serde(with = "DurationDef")] pub timeout: std::time::Duration, } ``` --- ## 2. clap ### Define CLI with Derive API ```rust use clap::{Args, Parser, Subcommand, ValueEnum}; #[derive(Parser)] #[command(name = "mytool", version, about = "A tool that does things")] pub struct Cli { #[command(subcommand)] pub command: Commands, /// Increase verbosity (-v, -vv, -vvv) #[arg(short, long, action = clap::ArgAction::Count, global = true)] pub verbose: u8, /// Config file path #[arg(long, env = "MYTOOL_CONFIG", default_value = "config.toml", global = true)] pub config: std::path::PathBuf, } #[derive(Subcommand)] pub enum Commands { /// Fetch data from the server Fetch(FetchArgs), /// Push data to the server Push(PushArgs), } #[derive(Args)] pub struct FetchArgs { /// Target URL #[arg(value_parser = parse_url)] pub url: url::Url, /// Output format #[arg(long, value_enum, default_value_t = OutputFormat::Json)] pub format: OutputFormat, /// Optional tags (can be repeated) #[arg(long = "tag", short = 't')] pub tags: Vec<String>, /// Dry run mode #[arg(long, conflicts_with = "output")] pub dry_run: bool, /// Write output to file #[arg(long)] pub output: Option<std::path::PathBuf>, } #[derive(ValueEnum, Clone)] pub enum OutputFormat { Json, Csv, Pretty, } fn parse_url(s: &str) -> Result<url::Url, String> { url::Url::parse(s).map_err(|e| e.to_string()) } ``` ### Parse and Dispatch ```rust fn main() { let cli = Cli::parse(); match cli.command { Commands::Fetch(args) => run_fetch(args, cli.verbose), Commands::Push(args) => run_push(args, cli.verbose), } } ``` ### Generate Shell Completions ```rust use clap::CommandFactory; use clap_complete::{generate, Shell}; fn print_completions(shell: Shell) { let mut cmd = Cli::command(); generate(shell, &mut cmd, "mytool", &mut std::io::stdout()); } ``` --- ## 3. reqwest ### Build a Shared Client ```rust use reqwest::{Client, ClientBuilder, header}; use std::time::Duration; fn build_client(base_token: &str) -> reqwest::Result<Client> { let mut headers = header::HeaderMap::new(); let auth = header::HeaderValue::from_str(&format!("Bearer {}", base_token)) .expect("Invalid token"); headers.insert(header::AUTHORIZATION, auth); ClientBuilder::new() .timeout(Duration::from_secs(30)) .connect_timeout(Duration::from_secs(5)) .default_headers(headers) .user_agent("myapp/1.0") .build() } ``` ### Send GET / POST / PUT Requests ```rust use serde::{Deserialize, Serialize}; #[derive(Deserialize)] struct ApiResponse { data: Vec<Item> } #[derive(Serialize)] struct CreateRequest { name: String, value: u32 } async fn fetch_items(client: &Client, url: &str) -> anyhow::Result<Vec<Item>> { let resp = client .get(url) .query(&[("limit", "100"), ("page", "1")]) .send() .await? .error_for_status()? .json::<ApiResponse>() .await?; Ok(resp.data) } async fn create_item(client: &Client, url: &str, name: &str) -> anyhow::Result<Item> { let body = CreateRequest { name: name.to_string(), value: 42 }; client .post(url) .json(&body) .send() .await? .error_for_status()? .json::<Item>() .await .map_err(Into::into) } ``` ### Upload Multipart Form ```rust use reqwest::multipart; async fn upload_file(client: &Client, url: &str, path: &std::path::Path) -> anyhow::Result<()> { let file_bytes = tokio::fs::read(path).await?; let filename = path.file_name().unwrap().to_string_lossy().into_owned(); let part = multipart::Part::bytes(file_bytes) .file_name(filename) .mime_str("application/octet-stream")?; let form = multipart::Form::new() .text("description", "my upload") .part("file", part); client.post(url).multipart(form).send().await?.error_for_status()?; Ok(()) } ``` ### Stream a Response ```rust use futures_util::StreamExt; async fn stream_download(client: &Client, url: &str) -> anyhow::Result<Vec<u8>> { let mut stream = client.get(url).send().await?.bytes_stream(); let mut buf = Vec::new(); while let Some(chunk) = stream.next().await { buf.extend_from_slice(&chunk?); } Ok(buf) } ``` ### Retry with Exponential Backoff ```rust use std::time::Duration; async fn get_with_retry(client: &Client, url: &str, max: usize) -> anyhow::Result<String> { let mut delay = Duration::from_millis(200); for attempt in 0..max { match client.get(url).send().await?.error_for_status() { Ok(resp) => return Ok(resp.text().await?), Err(e) if attempt + 1 < max => { tokio::time::sleep(delay).await; delay *= 2; tracing::warn!(attempt, %e, "Retrying request"); } Err(e) => return Err(e.into()), } } unreachable!() } ``` --- ## 4. sqlx ### Set Up a Connection Pool ```toml # Cargo.toml sqlx = { version = "0.7", features = ["postgres", "runtime-tokio-rustls", "macros", "chrono", "uuid"] } ``` ```rust use sqlx::PgPool; pub async fn connect(database_url: &str) -> sqlx::Result<PgPool> { PgPool::connect(database_url).await } // Or with options use sqlx::postgres::PgPoolOptions; pub async fn connect_pool(database_url: &str) -> sqlx::Result<PgPool> { PgPoolOptions::new() .max_connections(20) .acquire_timeout(std::time::Duration::from_secs(5)) .connect(database_url) .await } ``` ### Write Compile-Time Checked Queries ```rust // DATABASE_URL must be set at compile time // sqlx::query! checks SQL against live schema pub async fn get_user(pool: &PgPool, id: i64) -> sqlx::Result<Option<User>> { sqlx::query_as!( User, r#"SELECT id, name, email, created_at FROM users WHERE id = $1"#, id ) .fetch_optional(pool) .await } pub async fn list_users(pool: &PgPool, limit: i64) -> sqlx::Result<Vec<User>> { sqlx::query_as!( User, r#"SELECT id, name, email, created_at FROM users ORDER BY id LIMIT $1"#, limit ) .fetch_all(pool) .await } ``` ### Derive `FromRow` ```rust use sqlx::FromRow; use chrono::{DateTime, Utc}; use uuid::Uuid; #[derive(Debug, FromRow)] pub struct User { pub id: i64, pub name: String, pub email: String, pub created_at: DateTime<Utc>, } // Override column name #[derive(Debug, FromRow)] pub struct Post { pub id: Uuid, #[sqlx(rename = "body_text")] pub body: String, // Skip a column that won't be in every query #[sqlx(skip)] pub computed: Option<String>, } ``` ### Use Transactions ```rust pub async fn transfer_funds( pool: &PgPool, from: i64, to: i64, amount: i64, ) -> sqlx::Result<()> { let mut tx = pool.begin().await?; sqlx::query!("UPDATE accounts SET balance = balance - $1 WHERE id = $2", amount, from) .execute(&mut *tx) .await?; sqlx::query!("UPDATE accounts SET balance = balance + $1 WHERE id = $2", amount, to) .execute(&mut *tx) .await?; tx.commit().await } ``` ### Map JSON Columns ```rust use serde::{Deserialize, Serialize}; use sqlx::types::Json; #[derive(Debug, Serialize, Deserialize)] pub struct Settings { pub theme: String, pub notifications: bool, } #[derive(Debug, FromRow)] pub struct UserWithSettings { pub id: i64, pub name: String, pub settings: Json<Settings>, // maps JSON column to typed struct } pub async fn get_settings(pool: &PgPool, id: i64) -> sqlx::Result<Settings> { let row = sqlx::query_as!( UserWithSettings, r#"SELECT id, name, settings AS "settings: Json<Settings>" FROM users WHERE id = $1"#, id ) .fetch_one(pool) .await?; Ok(row.settings.0) } ``` ### Run Migrations ```rust // Migrations live in ./migrations/*.sql, named 0001_create_users.sql etc. pub async fn run_migrations(pool: &PgPool) -> sqlx::Result<()> { sqlx::migrate!("./migrations").run(pool).await } ``` --- ## 5. axum ### Define a Router with State ```rust use axum::{Router, routing::{get, post}}; use std::sync::Arc; #[derive(Clone)] pub struct AppState { pub pool: sqlx::PgPool, pub config: Arc<Config>, } pub fn build_router(state: AppState) -> Router { Router::new() .route("/health", get(health_handler)) .route("/users", get(list_users).post(create_user)) .route("/users/:id", get(get_user).delete(delete_user)) .nest("/admin", admin_routes()) .layer(tower_http::trace::TraceLayer::new_for_http()) .with_state(state) } ``` ### Write Handlers with Extractors ```rust use axum::{ extract::{Path, Query, State, Json}, http::StatusCode, response::IntoResponse, }; use serde::Deserialize; #[derive(Deserialize)] pub struct Pagination { pub page: Option<u32>, pub limit: Option<u32>, } pub async fn list_users( State(state): State<AppState>, Query(params): Query<Pagination>, ) -> impl IntoResponse { let limit = params.limit.unwrap_or(20).min(100); let page = params.page.unwrap_or(0); match fetch_users(&state.pool, limit as i64, page as i64).await { Ok(users) => Json(users).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } #[derive(Deserialize)] pub struct CreateUserRequest { pub name: String, pub email: String, } pub async fn create_user( State(state): State<AppState>, Json(body): Json<CreateUserRequest>, ) -> Result<(StatusCode, Json<User>), AppError> { let user = insert_user(&state.pool, &body.name, &body.email).await?; Ok((StatusCode::CREATED, Json(user))) } ``` ### Define a Typed Error Response ```rust use axum::{http::StatusCode, response::{IntoResponse, Response}, Json}; use serde_json::json; pub enum AppError { NotFound(String), Database(sqlx::Error), Validation(String), } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, message) = match self { AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), AppError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg), AppError::Database(e) => { tracing::error!("Database error: {}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Internal error".to_string()) } }; (status, Json(json!({ "error": message }))).into_response() } } impl From<sqlx::Error> for AppError { fn from(e: sqlx::Error) -> Self { AppError::Database(e) } } ``` ### Handle WebSocket Connections ```rust use axum::extract::ws::{Message, WebSocket, WebSocketUpgrade}; pub async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { ws.on_upgrade(handle_socket) } async fn handle_socket(mut socket: WebSocket) { while let Some(msg) = socket.recv().await { match msg { Ok(Message::Text(text)) => { if socket.send(Message::Text(format!("echo: {text}"))).await.is_err() { break; } } Ok(Message::Close(_)) | Err(_) => break, _ => {} } } } ``` ### Gracefully Shut Down the Server ```rust use tokio::net::TcpListener; pub async fn serve(router: Router) -> anyhow::Result<()> { let listener = TcpListener::bind("0.0.0.0:3000").await?; axum::serve(listener, router) .with_graceful_shutdown(shutdown_signal()) .await?; Ok(()) } async fn shutdown_signal() { tokio::signal::ctrl_c().await.expect("Failed to install Ctrl+C handler"); tracing::info!("Shutdown signal received"); } ``` --- ## 6. tracing ### Set Up a Subscriber ```rust use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; pub fn init_tracing() { tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) .with(tracing_subscriber::fmt::layer().with_target(true)) .init(); } // For JSON output (structured logging in prod) pub fn init_json_tracing() { tracing_subscriber::registry() .with(EnvFilter::try_from_default_env().unwrap_or_else(|_| "info".into())) .with(tracing_subscriber::fmt::layer().json()) .init(); } ``` ### Instrument Functions ```rust use tracing::{instrument, info, warn, error, debug, Span}; #[instrument(skip(pool), fields(user_id = %id))] pub async fn get_user_profile(pool: &PgPool, id: i64) -> anyhow::Result<Profile> { debug!("Fetching user profile"); let profile = sqlx::query_as!(Profile, "SELECT * FROM profiles WHERE user_id = $1", id) .fetch_optional(pool) .await? .ok_or_else(|| anyhow::anyhow!("Profile not found"))?; info!(email = %profile.email, "Profile fetched"); Ok(profile) } ``` ### Create and Enter Spans Manually ```rust use tracing::{info_span, Instrument}; pub async fn process_batch(items: Vec<Item>) { for item in items { let span = info_span!("process_item", item_id = %item.id, kind = %item.kind); async move { tracing::info!("Processing item"); // work... tracing::info!("Item done"); } .instrument(span) .await; } } ``` ### Add Structured Fields to Events ```rust use tracing::info; pub fn log_request(method: &str, path: &str, status: u16, latency_ms: u64) { info!( http.method = method, http.path = path, http.status = status, latency_ms = latency_ms, "Request completed" ); } ``` ### Filter by Directive ``` # Environment variable examples RUST_LOG=info RUST_LOG=myapp=debug,sqlx=warn,tower_http=info RUST_LOG=debug,hyper=off ``` --- ## 7. rayon ### Parallelize with `par_iter` ```rust use rayon::prelude::*; fn sum_squares(numbers: &[f64]) -> f64 { numbers.par_iter().map(|x| x * x).sum() } fn find_primes(limit: u64) -> Vec<u64> { (2..limit) .into_par_iter() .filter(|&n| is_prime(n)) .collect() } fn transform_records(records: Vec<Record>) -> Vec<Output> { records.into_par_iter().map(process_record).collect() } ``` ### Build a Custom Thread Pool ```rust use rayon::ThreadPoolBuilder; fn main() { let pool = ThreadPoolBuilder::new() .num_threads(4) .thread_name(|i| format!("worker-{}", i)) .build() .unwrap(); pool.install(|| { (0..1000).into_par_iter().for_each(|i| { println!("Processing {}", i); }); }); } ``` ### Bridge Sequential Iterators ```rust use rayon::iter::ParallelBridge; fn process_lines(reader: impl std::io::BufRead + Send) -> Vec<String> { reader .lines() .par_bridge() // converts Iterator -> ParallelIterator .filter_map(|l| l.ok()) .map(|l| l.trim().to_string()) .collect() } ``` ### Know When to Choose rayon vs tokio | Situation | Use | |-----------|-----| | CPU-bound work (parsing, compression, crypto) | `rayon` | | I/O-bound work (network, disk, database) | `tokio` | | Mix: CPU work inside async | `tokio::task::spawn_blocking` with rayon inside | | Parallel collection transforms | `rayon` | | Concurrent HTTP requests | `tokio` + `FuturesUnordered` | ```rust // Offload rayon work from async context pub async fn heavy_computation(data: Vec<u8>) -> Vec<u8> { tokio::task::spawn_blocking(move || { data.par_iter().map(|b| b.wrapping_add(1)).collect() }) .await .expect("blocking task panicked") } ``` --- ## 8. itertools ```toml itertools = "0.12" ``` ### Use Useful Combinators ```rust use itertools::Itertools; fn demonstrate_itertools() { let nums = vec![1, 2, 3, 4, 5, 6]; // chunks - fixed-size non-overlapping groups for chunk in &nums.iter().chunks(2) { let v: Vec<_> = chunk.collect(); println!("{:?}", v); // [1,2], [3,4], [5,6] } // tuple_windows - sliding window of tuples let pairs: Vec<_> = nums.iter().tuple_windows::<(_, _)>().collect(); // [(1,2), (2,3), (3,4), (4,5), (5,6)] // group_by - consecutive runs (like Unix uniq) let words = vec!["a", "a", "b", "b", "b", "c"]; for (key, group) in &words.iter().group_by(|w| *w) { println!("{}: {}", key, group.count()); } // join - format with separator (no trailing sep) let s = ["foo", "bar", "baz"].iter().join(", "); // "foo, bar, baz" // sorted_by - sort without mutating let sorted = nums.iter().sorted_by(|a, b| b.cmp(a)).collect_vec(); // dedup - remove consecutive duplicates let deduped: Vec<_> = vec![1, 1, 2, 3, 3, 3, 4].into_iter().dedup().collect(); // [1, 2, 3, 4] // interleave - alternating elements let a = vec![1, 3, 5]; let b = vec![2, 4, 6]; let merged: Vec<_> = a.into_iter().interleave(b.into_iter()).collect(); // [1, 2, 3, 4, 5, 6] // unique - deduplicate (not just consecutive) let unique: Vec<_> = vec![1, 2, 1, 3, 2, 4].into_iter().unique().collect(); // [1, 2, 3, 4] // combinations and permutations let combos: Vec<_> = (0..4).combinations(2).collect(); // [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]] // partition_map - split into two collections let (evens, odds): (Vec<_>, Vec<_>) = nums.iter().partition_map(|&n| { if n % 2 == 0 { itertools::Either::Left(n) } else { itertools::Either::Right(n) } }); } ``` --- ## 9. Cow ### Understand Clone-on-Write `Cow<'a, B>` is either `Borrowed(&'a B)` or `Owned(B::Owned)`. Derefs to `&B` in both cases. Allocates only when you need to mutate. ### Use Cow in Function Signatures ```rust use std::borrow::Cow; // Accepts both &str and String, returns without allocating if no change needed fn normalize(input: &str) -> Cow<str> { if input.chars().all(|c| c.is_lowercase()) { Cow::Borrowed(input) // zero allocation } else { Cow::Owned(input.to_lowercase()) // allocates only when needed } } // Accept Cow to handle both owned and borrowed callers fn process(name: Cow<str>) { println!("Processing: {}", name); } // Call sites process(Cow::Borrowed("hello")); process(Cow::Owned(String::from("world"))); process(normalize("Mixed")); ``` ### Enable Zero-Copy Parsing ```rust use serde::Deserialize; // serde can borrow from input bytes when possible #[derive(Deserialize)] pub struct Request<'a> { #[serde(borrow)] pub method: Cow<'a, str>, // borrows from JSON bytes if no escaping needed pub id: u64, } ``` ### Build Efficient Builders with Cow ```rust use std::borrow::Cow; pub struct Query<'a> { table: Cow<'a, str>, conditions: Vec<Cow<'a, str>>, } impl<'a> Query<'a> { pub fn from_table(table: impl Into<Cow<'a, str>>) -> Self { Query { table: table.into(), conditions: vec![] } } pub fn where_clause(mut self, cond: impl Into<Cow<'a, str>>) -> Self { self.conditions.push(cond.into()); self } pub fn build(&self) -> String { if self.conditions.is_empty() { format!("SELECT * FROM {}", self.table) } else { format!("SELECT * FROM {} WHERE {}", self.table, self.conditions.iter().join(" AND ")) } } } ``` -
error-handling.md 15 KB
# Rust Error Handling Reference ## Table of Contents 1. [Result and Option](#1-result-and-option) 2. [The ? Operator](#2-the--operator) 3. [thiserror](#3-thiserror) 4. [anyhow](#4-anyhow) 5. [Custom Error Enums](#5-custom-error-enums) 6. [Error Conversion](#6-error-conversion) 7. [Error Context](#7-error-context) 8. [panic vs Result](#8-panic-vs-result) 9. [Result in main](#9-result-in-main) 10. [Anti-Patterns](#10-anti-patterns) --- ## 1. Result and Option ### Basics ```rust // Result<T, E>: Ok(T) on success, Err(E) on failure fn parse_port(s: &str) -> Result<u16, std::num::ParseIntError> { s.parse::<u16>() } // Option<T>: Some(T) or None fn find_user(id: u64) -> Option<User> { users.get(&id).cloned() } ``` ### map and and_then ```rust // map: transform Ok/Some without touching Err/None let doubled: Option<i32> = Some(5).map(|x| x * 2); // Some(10) let upper: Result<String, _> = Ok("hi").map(|s: &str| s.to_uppercase()); // and_then: chain fallible operations (flatMap) fn load_config(path: &str) -> Result<Config, Error> { read_file(path) .and_then(|contents| parse_toml(&contents)) .and_then(|raw| validate_config(raw)) } // Option::and_then for chaining lookups let city = get_user(id) .and_then(|user| get_address(user.address_id)) .and_then(|addr| addr.city); ``` ### unwrap_or and unwrap_or_else ```rust // unwrap_or: provide a fallback value (evaluated eagerly) let port: u16 = parse_port(s).unwrap_or(8080); let name: String = maybe_name.unwrap_or_else(String::new); // unwrap_or_else: provide a closure (evaluated lazily — prefer for expensive defaults) let config = load_config("app.toml") .unwrap_or_else(|_| Config::default()); // unwrap_or_default: use the Default impl let value: Vec<u8> = maybe_bytes.unwrap_or_default(); ``` ### ok_or and transpose ```rust // ok_or: convert Option into Result let user = find_user(id).ok_or(Error::UserNotFound(id))?; // ok_or_else: lazy version let user = find_user(id) .ok_or_else(|| Error::UserNotFound(id))?; // transpose: flip Option<Result<T, E>> <-> Result<Option<T>, E> let maybe_result: Option<Result<u32, _>> = Some("42".parse()); let result_maybe: Result<Option<u32>, _> = maybe_result.transpose(); // Ok(Some(42)) // Useful in iterators when you want the first error or all-None let parsed: Result<Vec<u32>, _> = strings .iter() .map(|s| s.parse::<u32>()) .collect(); // fails on first parse error ``` ### Other Useful Methods ```rust // is_ok, is_err, is_some, is_none if result.is_err() { log_failure(); } // map_err: transform only the error type let result = op().map_err(|e| format!("Operation failed: {e}")); // or / or_else: provide alternative on failure let result = primary().or_else(|_| fallback()); // inspect / inspect_err: side effects without consuming let result = load().inspect(|v| tracing::debug!(?v, "loaded")) .inspect_err(|e| tracing::warn!(?e, "load failed")); // flatten: Option<Option<T>> -> Option<T>, Result<Result<T,E>,E> -> Result<T,E> let flat: Option<i32> = Some(Some(5)).flatten(); // Some(5) ``` --- ## 2. The ? Operator ### Result Propagation ```rust // ? desugars to: match on Err, call From::from on the error, return early fn read_config(path: &str) -> Result<Config, AppError> { let text = std::fs::read_to_string(path)?; // io::Error -> AppError via From let config: Config = toml::from_str(&text)?; // toml::Error -> AppError via From Ok(config) } ``` ### Option Propagation ```rust // ? on Option returns None immediately (requires the function to return Option) fn first_line_word(text: &str) -> Option<&str> { text.lines().next()?.split_whitespace().next() } // Cannot mix Option? and Result? in the same function without conversion // Use .ok_or() or .ok_or_else() to convert Option -> Result fn find_section(text: &str) -> Result<&str, AppError> { text.lines() .find(|l| l.starts_with('[')) .ok_or(AppError::NoSection)? .trim() .into() } ``` ### From Conversion ```rust // ? calls From::from automatically. Define From impls to unlock ? impl From<std::io::Error> for AppError { fn from(e: std::io::Error) -> Self { AppError::Io(e) } } // Now io::Error can be converted with ? fn write_output(data: &[u8]) -> Result<(), AppError> { std::fs::write("out.bin", data)?; // io::Error converted automatically Ok(()) } ``` ### Early Return Pattern ```rust // ? enables clean early-return without match chains fn process(input: &str) -> Result<Output, AppError> { let parsed = parse(input)?; let validated = validate(parsed)?; let enriched = enrich(validated)?; Ok(transform(enriched)) } ``` --- ## 3. thiserror thiserror generates `std::error::Error` impls via derive macros. Use it in **libraries**. ### Derive Error and Format Messages ```rust use thiserror::Error; #[derive(Debug, Error)] pub enum AppError { #[error("user {id} not found")] UserNotFound { id: u64 }, #[error("invalid email address: {0}")] InvalidEmail(String), #[error("timeout after {0:?}")] Timeout(std::time::Duration), #[error("internal error")] Internal, } ``` ### #[from] for Automatic Conversion ```rust #[derive(Debug, Error)] pub enum AppError { // #[from] generates From<io::Error> for AppError #[error("IO error: {0}")] Io(#[from] std::io::Error), // Enables ? on sqlx operations automatically #[error("database error: {0}")] Database(#[from] sqlx::Error), #[error("serialization error: {0}")] Json(#[from] serde_json::Error), } ``` ### #[source] for Error Chains ```rust #[derive(Debug, Error)] pub enum AppError { // #[source] exposes inner error via Error::source() // #[from] implies #[source] automatically #[error("config load failed")] Config { #[source] cause: std::io::Error, }, // transparent: delegate Display and source to inner error #[error(transparent)] Other(#[from] anyhow::Error), } ``` ### Struct Errors with thiserror ```rust #[derive(Debug, Error)] #[error("parse failed at line {line}: {message}")] pub struct ParseError { pub line: usize, pub message: String, #[source] pub cause: Option<std::num::ParseIntError>, } ``` --- ## 4. anyhow anyhow provides a single opaque error type for **application** (binary) code. ### anyhow::Result and anyhow!() ```rust use anyhow::{anyhow, bail, ensure, Context, Result}; fn load(path: &str) -> Result<Config> { let text = std::fs::read_to_string(path)?; // any error works with ? let config = serde_json::from_str(&text)?; Ok(config) } // anyhow!() creates an ad-hoc error fn validate(n: i32) -> Result<i32> { if n < 0 { return Err(anyhow!("expected non-negative, got {n}")); } Ok(n) } ``` ### bail! and ensure! ```rust fn process(value: i32) -> Result<()> { // bail!() is return Err(anyhow!(...)) if value > 1000 { bail!("value {value} exceeds maximum of 1000"); } // ensure!() is if !condition { bail!(...) } ensure!(value >= 0, "value must be non-negative, got {value}"); Ok(()) } ``` ### .context() and .with_context() ```rust fn init() -> Result<()> { let config = std::fs::read_to_string("config.toml") .context("failed to read config.toml")?; // with_context: lazy, use when message is expensive to build let parsed: Config = toml::from_str(&config) .with_context(|| format!("failed to parse config (len={})", config.len()))?; Ok(()) } ``` ### Downcasting ```rust fn handle(err: anyhow::Error) { // Check if the underlying error is a specific type if let Some(io_err) = err.downcast_ref::<std::io::Error>() { eprintln!("IO error: {io_err}"); } else { eprintln!("Unknown error: {err:#}"); } } // {:#} prints the full error chain // {:?} prints the debug representation including backtrace ``` --- ## 5. Custom Error Enums ### Design Error Hierarchies ```rust // Top-level public error: coarse-grained, stable API surface #[derive(Debug, Error)] pub enum ServiceError { #[error("authentication failed")] Auth(#[from] AuthError), #[error("database unavailable")] Database(#[from] DbError), #[error("request invalid: {0}")] Validation(String), } // Sub-module error: fine-grained, internal #[derive(Debug, Error)] pub enum AuthError { #[error("token expired")] TokenExpired, #[error("invalid signature")] BadSignature, #[error("user {0} locked")] AccountLocked(u64), } ``` ### When to Split vs Combine ```rust // SPLIT when: // - Callers need to pattern-match specific variants // - Different modules own different error domains // - You want stable public API with internal flexibility // COMBINE (single enum) when: // - Small codebase with few error kinds // - Errors don't need distinct handling by callers // - Internal-only code // Guideline: one error enum per public API boundary (crate, module, trait) ``` --- ## 6. Error Conversion ### impl From ```rust impl From<std::io::Error> for AppError { fn from(e: std::io::Error) -> Self { match e.kind() { std::io::ErrorKind::NotFound => AppError::NotFound, std::io::ErrorKind::PermissionDenied => AppError::Forbidden, _ => AppError::Io(e), } } } ``` ### Manual Conversion ```rust // When From is too broad, convert explicitly with map_err fn read_key(path: &str) -> Result<Vec<u8>, AppError> { std::fs::read(path).map_err(|e| { if e.kind() == std::io::ErrorKind::NotFound { AppError::KeyMissing(path.to_string()) } else { AppError::Io(e) } }) } ``` ### Converting Between Error Crates ```rust // thiserror library error -> anyhow application error: just use ? // anyhow error -> thiserror: use #[error(transparent)] or explicit wrapping #[derive(Debug, Error)] pub enum AppError { #[error(transparent)] Internal(#[from] anyhow::Error), } // Or convert with a helper fn wrap(e: anyhow::Error) -> AppError { AppError::Internal(e) } ``` --- ## 7. Error Context ### Add Context Without Losing Source ```rust // anyhow .context() preserves the original error as source let data = fetch(url).context("failed to fetch user data")?; // thiserror: wrap in a variant with #[source] #[derive(Debug, Error)] pub enum LoadError { #[error("failed to read {path}")] Read { path: String, #[source] cause: std::io::Error, }, } fn load(path: &str) -> Result<Vec<u8>, LoadError> { std::fs::read(path).map_err(|cause| LoadError::Read { path: path.to_string(), cause, }) } ``` ### Wrapping Strategy ```rust // Layer context at each boundary crossing // 1. Low-level: return raw errors with thiserror // 2. Service layer: add domain context with .context() // 3. Handler/main: print full chain with {:#} fn read_user(id: u64) -> Result<User> { let row = db.query_one(id) .with_context(|| format!("db lookup failed for user {id}"))?; parse_user(row) .with_context(|| format!("failed to parse user {id} from db row")) } ``` --- ## 8. panic vs Result ### When panic Is Legitimate ```rust // 1. Tests: use assert!, assert_eq!, unwrap() freely #[test] fn test_parse() { assert_eq!(parse("42").unwrap(), 42); } // 2. Initialization that cannot recover fn main() { let config = load_config().expect("failed to load required config"); } // 3. Invariant violations that indicate a programmer bug fn get_first(v: &[i32]) -> i32 { // Caller contract: v must not be empty v[0] // panics on empty — that is correct behaviour } // 4. Prototype / throwaway code (use todo!, unimplemented!) fn not_implemented_yet() -> String { todo!("implement serialization") } ``` ### catch_unwind for Panic Isolation ```rust use std::panic; // Catch panics from untrusted code (plugin, FFI boundary) let result = panic::catch_unwind(|| { potentially_panicking_code() }); match result { Ok(value) => println!("success: {value:?}"), Err(_) => eprintln!("caught a panic"), } // Note: catch_unwind does NOT catch abort-mode panics or stack overflows ``` --- ## 9. Result in main ### Return Result from main ```rust // main can return Result<(), E> where E: Debug fn main() -> Result<(), Box<dyn std::error::Error>> { let config = load_config()?; run(config)?; Ok(()) } // With anyhow for full error chains fn main() -> anyhow::Result<()> { let config = load_config().context("startup failed")?; run(config)?; Ok(()) } ``` ### ExitCode and process::exit ```rust use std::process::ExitCode; fn main() -> ExitCode { match run() { Ok(()) => ExitCode::SUCCESS, Err(e) => { eprintln!("error: {e:#}"); ExitCode::FAILURE } } } // process::exit for immediate termination (skips destructors) fn must_succeed() { if let Err(e) = critical_setup() { eprintln!("fatal: {e}"); std::process::exit(1); } } ``` ### Termination Trait ```rust // For custom exit codes beyond 0/1 use std::process::{ExitCode, Termination}; struct AppExit(u8); impl Termination for AppExit { fn report(self) -> ExitCode { ExitCode::from(self.0) } } fn main() -> AppExit { match run() { Ok(()) => AppExit(0), Err(AppError::ConfigMissing) => AppExit(2), Err(_) => AppExit(1), } } ``` --- ## 10. Anti-Patterns ### .unwrap() Everywhere ```rust // BAD: panics on any error in production let text = std::fs::read_to_string("config.toml").unwrap(); let user = find_user(id).unwrap(); // GOOD: propagate with ?, provide defaults, or handle explicitly let text = std::fs::read_to_string("config.toml") .context("config.toml is required")?; let user = find_user(id).ok_or(AppError::UserNotFound(id))?; ``` ### Stringly Typed Errors ```rust // BAD: callers cannot inspect or match on error kind fn load(path: &str) -> Result<Data, String> { std::fs::read_to_string(path).map_err(|e| e.to_string()) } // GOOD: typed errors callers can handle fn load(path: &str) -> Result<Data, AppError> { let text = std::fs::read_to_string(path)?; Ok(parse(&text)?) } ``` ### Excessive Error Types ```rust // BAD: one error type per function — impossible to use fn read_name() -> Result<String, ReadNameError> { ... } fn parse_age() -> Result<u32, ParseAgeError> { ... } fn validate() -> Result<(), ValidateError> { ... } // GOOD: one error type per domain boundary fn load_user(id: u64) -> Result<User, UserError> { ... } ``` ### Ignoring Errors with let _ = ```rust // BAD: silently discards errors — hides bugs let _ = send_notification(user); let _ = std::fs::remove_file(tmp); // GOOD: log or explicitly decide to ignore if let Err(e) = send_notification(user) { tracing::warn!(?e, "notification failed, continuing"); } // If truly safe to ignore, be explicit about why std::fs::remove_file(tmp).ok(); // .ok() signals intentional ignore ``` ### Boxing Without Cause ```rust // BAD: loses type information, callers can't downcast easily fn run() -> Result<(), Box<dyn std::error::Error>> { ... } // GOOD in main / test harnesses, BAD in library APIs // For libraries, use typed errors via thiserror // For applications, use anyhow::Result ``` -
ownership-lifetimes.md 16.3 KB
# Ownership and Lifetimes Reference ## Table of Contents 1. [Move Semantics](#1-move-semantics) 2. [Borrowing Rules](#2-borrowing-rules) 3. [Lifetime Annotations](#3-lifetime-annotations) 4. [Lifetime Elision Rules](#4-lifetime-elision-rules) 5. [static Lifetime](#5-static-lifetime) 6. [Interior Mutability](#6-interior-mutability) 7. [Common Borrow Checker Patterns](#7-common-borrow-checker-patterns) 8. [NLL (Non-Lexical Lifetimes)](#8-nll-non-lexical-lifetimes) 9. [Self-Referential Structs](#9-self-referential-structs) --- ## 1. Move Semantics ### Understand What Moves vs What Copies Types that implement `Copy` are implicitly duplicated on assignment. All others are moved. **Copy types:** All integer primitives, `f32`/`f64`, `bool`, `char`, raw pointers, references (`&T`), arrays of Copy types, tuples of Copy types. **Move types:** `String`, `Vec<T>`, `Box<T>`, `HashMap`, any struct containing a move type. ```rust // Copy - both variables remain valid let x: i32 = 5; let y = x; println!("{} {}", x, y); // OK // Move - s1 is no longer valid after assignment let s1 = String::from("hello"); let s2 = s1; // println!("{}", s1); // ERROR: value moved // Clone to keep both let s3 = String::from("hello"); let s4 = s3.clone(); println!("{} {}", s3, s4); // OK ``` ### Recognize Moves in Function Calls Passing a move type to a function moves ownership into that function. The caller loses access. ```rust fn consume(s: String) { println!("{}", s); } // s is dropped here fn borrow(s: &String) { println!("{}", s); } // s is NOT dropped; caller retains ownership fn main() { let s = String::from("hello"); borrow(&s); // s still valid consume(s); // s moved into consume // consume(s); // ERROR: s already moved } ``` ### Handle Moves in Closures Closures capture variables by the minimum required (reference, mutable reference, or move). Use `move` to force ownership transfer. ```rust let s = String::from("hello"); // Closure borrows s by reference (default when possible) let print = || println!("{}", s); print(); println!("{}", s); // s still valid // Force move into closure (required for threads) let s2 = String::from("world"); let owned = move || println!("{}", s2); // println!("{}", s2); // ERROR: s2 moved into closure owned(); ``` Closures sent to threads must own their data because the thread may outlive the caller's stack: ```rust let data = vec![1, 2, 3]; std::thread::spawn(move || { println!("{:?}", data); // data moved into thread }); ``` ### Avoid Moves in Loops Moving a value inside a loop consumes it on the first iteration. Use references or `clone` strategically. ```rust let items = vec![String::from("a"), String::from("b")]; // BAD: moves items on first iteration if iterating by value // for item in items { ... } // items consumed after loop // GOOD: iterate by reference for item in &items { println!("{}", item); } println!("{:?}", items); // items still valid // GOOD: when you need ownership, iterate by value and handle each for item in items { process(item); // each item moved individually, that's fine } ``` --- ## 2. Borrowing Rules ### Apply the Core Rules 1. At any point, you may have either one `&mut T` or any number of `&T` references — never both simultaneously. 2. References must always point to valid data (no dangling references). ```rust let mut s = String::from("hello"); let r1 = &s; let r2 = &s; // let r3 = &mut s; // ERROR: cannot borrow as mutable while borrowed as immutable println!("{} {}", r1, r2); // r1 and r2 go out of scope here (NLL) let r3 = &mut s; // OK now r3.push_str("!"); ``` ### Understand Reborrowing A `&mut T` can be "reborrowed" as `&T` or a shorter-lived `&mut T`. The compiler inserts reborrows automatically in most cases. ```rust fn modify(s: &mut String) { // Reborrow: passing &mut *s creates a new &mut with shorter lifetime takes_str(&*s); // reborrow as &str s.push_str("!"); // original mutable ref still usable after reborrow ends } fn takes_str(s: &str) { println!("{}", s); } ``` ### Recognize Temporary Borrows Method calls that return references extend the borrow of `self` for the duration the reference is held. ```rust let mut map: HashMap<&str, Vec<i32>> = HashMap::new(); map.insert("key", vec![1, 2, 3]); // This holds an immutable borrow of map via get() let val = map.get("key").unwrap(); println!("{:?}", val); // val borrow ends here map.insert("other", vec![4]); // OK: no active borrows ``` --- ## 3. Lifetime Annotations ### Read Lifetime Syntax Lifetime parameters start with `'` and appear in angle brackets. They describe relationships, not durations. ```rust // 'a is a generic lifetime parameter fn longest<'a>(x: &'a str, y: &'a str) -> &'a str { if x.len() > y.len() { x } else { y } } ``` The annotation says: "the returned reference lives at least as long as the shorter of x and y." ### Annotate Function Lifetimes Only annotate when the compiler cannot infer the relationship (multiple input references, output borrows from one of them). ```rust // Input and output tied to first argument only fn first_word<'a>(s: &'a str) -> &'a str { s.split_whitespace().next().unwrap_or("") } // Two unrelated input lifetimes fn split_at<'a, 'b>(s: &'a str, _sep: &'b str) -> (&'a str, &'a str) { let mid = s.len() / 2; (&s[..mid], &s[mid..]) } // Output may come from either input - must unify lifetimes fn pick<'a>(a: &'a str, b: &'a str, use_a: bool) -> &'a str { if use_a { a } else { b } } ``` ### Annotate Struct Lifetimes Structs holding references must declare the lifetime of those references. ```rust struct Excerpt<'a> { text: &'a str, } impl<'a> Excerpt<'a> { // &self lifetime elided (elision rule 3) fn content(&self) -> &str { self.text } // Must annotate: output could be self.text or announcement fn announce<'b>(&'a self, announcement: &'b str) -> &'a str { println!("{}", announcement); self.text } } ``` ### Use Multiple Lifetime Parameters Use multiple parameters when outputs have different source lifetimes. ```rust struct Cache<'data, 'key> { data: &'data [u8], key: &'key str, } // 'long outlives 'short: items from 'long can be stored where 'short is needed fn merge<'long: 'short, 'short>( primary: &'long str, fallback: &'short str, use_primary: bool, ) -> &'short str { if use_primary { primary } else { fallback } } ``` --- ## 4. Lifetime Elision Rules ### Apply the Three Elision Rules The compiler applies these rules in order before requiring annotations: **Rule 1:** Each reference parameter gets its own distinct lifetime. ```rust fn foo(x: &str, y: &str) -> &str // becomes: fn foo<'a, 'b>(x: &'a str, y: &'b str) -> &??? str // output lifetime unknown - annotation required ``` **Rule 2:** If there is exactly one input lifetime, it applies to all outputs. ```rust fn first_word(s: &str) -> &str // becomes: fn first_word<'a>(s: &'a str) -> &'a str // inferred ``` **Rule 3:** If one of the inputs is `&self` or `&mut self`, the output gets self's lifetime. ```rust impl Foo { fn bar(&self, x: &str) -> &str // becomes: fn bar<'a, 'b>(&'a self, x: &'b str) -> &'a str // inferred } ``` ### Know When to Annotate Annotate when: - Multiple input references and the output could come from more than one of them - A struct holds a reference - You need to express a lifetime bound (`T: 'a`) Omit when: - Single input reference (rule 2 applies) - Method returning reference derived from `&self` (rule 3 applies) - Output is an owned type (no lifetime needed) --- ## 5. 'static Lifetime ### Understand String Literals vs Owned Data `&'static str` means the reference points to data embedded in the binary — always valid. ```rust let s: &'static str = "I am in the binary"; // string literal // Owned String is NOT 'static, but can produce &str with any lifetime let owned = String::from("dynamic"); let borrowed: &str = &owned; // lifetime tied to owned, not 'static ``` ### Correct the T: 'static Misconception `T: 'static` does NOT mean T lives forever. It means T contains no non-static references — T may be dropped at any time. ```rust // T: 'static = T owns all its data (no borrowed references inside) fn store<T: 'static>(val: T) { std::thread::spawn(move || drop(val)); // safe: T outlives any borrow } store(String::from("owned")); // OK: String owns its data store(42i32); // OK: Copy type, no references let s = String::from("temp"); // store(&s); // ERROR: &s has lifetime tied to s, not 'static ``` ### Use 'static in Error and Trait Objects Error types commonly require `'static` so they can be sent across threads or stored. ```rust fn might_fail() -> Result<(), Box<dyn std::error::Error + 'static>> { std::fs::read_to_string("missing.txt")?; Ok(()) } // Thread-sendable trait object fn run_task(task: Box<dyn Fn() + Send + 'static>) { std::thread::spawn(task); } ``` --- ## 6. Interior Mutability ### Use Cell<T> for Copy Types `Cell<T>` allows mutation through a shared reference. It is `!Sync` (single-threaded only) and works only for `Copy` types. ```rust use std::cell::Cell; struct Counter { count: Cell<u32>, } impl Counter { fn increment(&self) { // &self, not &mut self self.count.set(self.count.get() + 1); } fn value(&self) -> u32 { self.count.get() } } let c = Counter { count: Cell::new(0) }; c.increment(); c.increment(); println!("{}", c.value()); // 2 ``` ### Use RefCell<T> for Non-Copy Types `RefCell<T>` enforces borrow rules at runtime. Panics if rules are violated. Also `!Sync`. ```rust use std::cell::RefCell; let data = RefCell::new(vec![1, 2, 3]); // Immutable borrow let r = data.borrow(); println!("{:?}", *r); drop(r); // release before mutable borrow // Mutable borrow data.borrow_mut().push(4); // try_borrow / try_borrow_mut avoid panics match data.try_borrow_mut() { Ok(mut v) => v.push(5), Err(_) => eprintln!("already borrowed"), } ``` Common pattern: `Rc<RefCell<T>>` for shared, mutable ownership in single-threaded code. ```rust use std::rc::Rc; use std::cell::RefCell; let shared = Rc::new(RefCell::new(vec![])); let clone = Rc::clone(&shared); shared.borrow_mut().push(1); clone.borrow_mut().push(2); println!("{:?}", shared.borrow()); // [1, 2] ``` ### Use OnceCell and OnceLock for Lazy Initialization `OnceCell<T>` initializes a value at most once. `OnceLock<T>` is the thread-safe version. ```rust use std::cell::OnceCell; struct Config { expensive: OnceCell<Vec<u8>>, } impl Config { fn data(&self) -> &Vec<u8> { self.expensive.get_or_init(|| { expensive_computation() }) } } // OnceLock for global statics (thread-safe) use std::sync::OnceLock; static INSTANCE: OnceLock<String> = OnceLock::new(); fn get_instance() -> &'static String { INSTANCE.get_or_init(|| String::from("initialized once")) } ``` ### Choose the Right Type | Type | Thread-safe | Works with | Runtime check | |------|-------------|------------|---------------| | `Cell<T>` | No | `Copy` types | No (get/set) | | `RefCell<T>` | No | Any `T` | Yes (panics) | | `OnceCell<T>` | No | Any `T` | No (init once) | | `OnceLock<T>` | Yes | Any `T: Send + Sync` | No (init once) | | `Mutex<T>` | Yes | Any `T: Send` | Blocks | | `RwLock<T>` | Yes | Any `T: Send + Sync` | Blocks | --- ## 7. Common Borrow Checker Patterns ### Split Borrows to Borrow Multiple Fields The borrow checker tracks fields independently. Access them through separate references. ```rust struct Point { x: f64, y: f64 } let mut p = Point { x: 1.0, y: 2.0 }; // ERROR: cannot borrow p.x as mutable because p is also borrowed // let rx = &mut p.x; // let ry = &mut p.y; // OK: split into two mutable references to distinct fields let rx = &mut p.x; let ry = &mut p.y; *rx += 1.0; *ry += 1.0; ``` For slices, use `split_at_mut`: ```rust let mut data = vec![1, 2, 3, 4, 5]; let (left, right) = data.split_at_mut(2); left[0] = 10; right[0] = 30; ``` ### Use Indices Instead of References When a data structure is being modified, holding an index avoids borrow conflicts. ```rust // BAD: first_ref holds a borrow while we try to modify vec // let first_ref = &vec[0]; // vec.push(99); // ERROR // GOOD: store index, re-access after modification let first_idx = 0; vec.push(99); println!("{}", vec[first_idx]); // re-borrow, no conflict ``` ### Use Temporary Variables to Shorten Borrow Scope Extracting a value before using it can satisfy the borrow checker. ```rust fn process(map: &mut HashMap<String, Vec<i32>>, key: &str) { // This fails: cannot borrow map as mutable while key is borrowed from it // if map.contains_key(key) { // map.get_mut(key).unwrap().push(1); // } // Clone the key to avoid holding a reference into map let key = key.to_string(); map.entry(key).or_default().push(1); } ``` ### Use the Entry API for Maps `entry` combines lookup and insert in one operation, avoiding double borrows. ```rust use std::collections::HashMap; let mut scores: HashMap<String, Vec<i32>> = HashMap::new(); // BAD: two separate borrows // if !scores.contains_key("Alice") { // scores.insert("Alice".to_string(), vec![]); // } // scores.get_mut("Alice").unwrap().push(10); // GOOD: entry API scores.entry("Alice".to_string()).or_default().push(10); scores.entry("Bob".to_string()).or_insert_with(Vec::new).push(5); // Modify existing or insert computed value scores.entry("Carol".to_string()) .and_modify(|v| v.push(99)) .or_insert_with(|| vec![0]); ``` ### Restructure Loops That Hold Borrows Collecting indices or keys before iterating avoids holding a reference during modification. ```rust let mut map: HashMap<i32, i32> = HashMap::new(); map.insert(1, 10); map.insert(2, 20); // Collect keys first, then iterate let keys: Vec<i32> = map.keys().cloned().collect(); for key in keys { if key % 2 == 0 { map.remove(&key); // OK: no active borrow from .keys() } } ``` --- ## 8. NLL (Non-Lexical Lifetimes) ### Understand What NLL Provides Before NLL (pre-2018 edition), borrows lasted until the end of the lexical block. NLL ends borrows at the last point of use. ```rust let mut s = String::from("hello"); let r = &s; println!("{}", r); // last use of r // Pre-NLL: ERROR here because r's scope extended to end of block // NLL: OK because r is no longer used after the println s.push_str(" world"); println!("{}", s); ``` ### Know NLL's Limits NLL does not help when a borrow is inside a loop or the returned reference ties back to self. ```rust // This still fails even with NLL - the borrow from get() ties to map fn first_or_insert(map: &mut HashMap<i32, i32>, key: i32) -> &i32 { if let Some(val) = map.get(&key) { return val; // borrows map } map.insert(key, 0); // ERROR: map already borrowed by return path map.get(&key).unwrap() } // Fix: use entry API fn first_or_insert_fixed(map: &mut HashMap<i32, i32>, key: i32) -> &i32 { map.entry(key).or_insert(0) } ``` --- ## 9. Self-Referential Structs ### Understand Why Self-Referential Structs Fail A struct cannot hold a reference to one of its own fields because moving the struct would invalidate the reference. ```rust // This does NOT compile struct SelfRef { data: String, ptr: &str, // would need lifetime tied to self.data — impossible } ``` ### Use the ouroboros Crate `ouroboros` generates safe self-referential structs via macro. ```rust // Cargo.toml: ouroboros = "0.18" use ouroboros::self_referencing; #[self_referencing] struct ParsedDocument { raw: String, #[borrows(raw)] #[covariant] parsed: Vec<&'this str>, } let doc = ParsedDocumentBuilder { raw: String::from("hello world foo"), parsed_builder: |raw: &str| raw.split_whitespace().collect(), }.build(); doc.with_parsed(|words| println!("{:?}", words)); ``` ### Use Pin for Futures and Async `Pin<P>` prevents moving the pinned value. The async runtime uses it to allow self-referential futures. ```rust use std::pin::Pin; use std::marker::PhantomPinned; struct Unmovable { data: String, // self_ref would point into data _pin: PhantomPinned, } // Create pinned on heap let pinned = Box::pin(Unmovable { data: String::from("hello"), _pin: PhantomPinned, }); // Can call methods through Pin // Cannot move out of Pin<Box<T>> if T: !Unpin ``` In practice, `Pin` appears most often in custom `Future` implementations and when building async combinators. Prefer `async fn` and existing executor abstractions over manual `Pin` management. -
testing.md 18.9 KB
# Rust Testing Reference ## Table of Contents 1. [Unit Tests](#1-unit-tests) 2. [Integration Tests](#2-integration-tests) 3. [Doc Tests](#3-doc-tests) 4. [Async Tests](#4-async-tests) 5. [mockall](#5-mockall) 6. [Test Fixtures](#6-test-fixtures) 7. [Property-Based Testing](#7-property-based-testing) 8. [Benchmarks](#8-benchmarks) 9. [Snapshot Testing](#9-snapshot-testing) 10. [Test Organization](#10-test-organization) 11. [CI Patterns](#11-ci-patterns) --- ## 1. Unit Tests ### Write Tests in `#[cfg(test)]` Modules ```rust pub fn divide(a: f64, b: f64) -> Result<f64, String> { if b == 0.0 { Err("division by zero".to_string()) } else { Ok(a / b) } } #[cfg(test)] mod tests { use super::*; // bring parent module into scope #[test] fn divide_positive_numbers() { assert_eq!(divide(10.0, 2.0), Ok(5.0)); } #[test] fn divide_returns_error_on_zero() { assert!(divide(1.0, 0.0).is_err()); } #[test] #[should_panic(expected = "index out of bounds")] fn panics_on_bad_index() { let v: Vec<i32> = vec![]; let _ = v[0]; } // Return Result from a test - failure message from the Err variant #[test] fn parse_valid_input() -> Result<(), String> { let n: i32 = "42".parse().map_err(|e: std::num::ParseIntError| e.to_string())?; assert_eq!(n, 42); Ok(()) } } ``` ### Use Assert Macros Effectively ```rust #[cfg(test)] mod tests { #[test] fn assert_variants() { let x = 5; assert!(x > 0); // boolean assert_eq!(x, 5); // equality (implements PartialEq + Debug) assert_ne!(x, 99); // inequality assert_eq!(x, 5, "Expected 5, got {}", x); // with message // Floating point - check within epsilon let f = 0.1 + 0.2; assert!((f - 0.3).abs() < 1e-10, "float comparison failed: {}", f); } } ``` --- ## 2. Integration Tests ### Organize Tests in the `tests/` Directory ``` my_crate/ ├── src/ │ └── lib.rs └── tests/ ├── common/ │ └── mod.rs # shared helpers (not a test file) ├── api_test.rs └── db_test.rs ``` ```rust // tests/common/mod.rs - shared setup, not discovered as a test binary pub fn setup_logging() { let _ = tracing_subscriber::fmt::try_init(); } pub fn load_fixture(name: &str) -> serde_json::Value { let path = std::path::Path::new("tests/fixtures").join(name); let bytes = std::fs::read(path).expect("fixture not found"); serde_json::from_slice(&bytes).expect("invalid fixture JSON") } ``` ```rust // tests/api_test.rs - each file becomes a separate test binary mod common; use my_crate::ApiClient; #[test] fn client_builds_with_defaults() { common::setup_logging(); let client = ApiClient::new("http://localhost"); assert_eq!(client.base_url(), "http://localhost"); } ``` ### Share State Between Integration Test Files ```rust // tests/common/mod.rs use std::sync::OnceLock; static SERVER: OnceLock<TestServer> = OnceLock::new(); pub fn get_server() -> &'static TestServer { SERVER.get_or_init(|| TestServer::start()) } ``` --- ## 3. Doc Tests ### Write Testable Examples in Documentation ```rust /// Parses a version string into major, minor, patch components. /// /// # Examples /// /// ``` /// use my_crate::parse_version; /// /// let (major, minor, patch) = parse_version("1.2.3").unwrap(); /// assert_eq!((major, minor, patch), (1, 2, 3)); /// ``` /// /// Returns `None` for invalid input: /// /// ``` /// use my_crate::parse_version; /// assert!(parse_version("not_a_version").is_none()); /// ``` pub fn parse_version(s: &str) -> Option<(u32, u32, u32)> { // ... } ``` ### Use Hidden Setup Lines ```rust /// Demonstrates the cache in action. /// /// ``` /// # use my_crate::Cache; /// # let mut cache = Cache::new(100); // hidden: sets up state /// cache.insert("key", "value"); /// assert_eq!(cache.get("key"), Some("value")); /// ``` ``` ### Mark Non-Runnable Examples ```rust /// Connect to the database. /// /// ```no_run /// # use my_crate::connect; /// // This compiles but does not run (needs a real database) /// let pool = connect("postgres://localhost/mydb").unwrap(); /// ``` /// /// This example is only shown, not compiled: /// /// ```ignore /// // Complex setup omitted /// some_impossible_setup(); /// ``` /// /// This example should fail to compile: /// /// ```compile_fail /// let x: u32 = "not a number"; // type error /// ``` ``` --- ## 4. Async Tests ### Test with `#[tokio::test]` ```rust #[tokio::test] async fn fetch_returns_data() { let client = build_client(); let result = client.fetch("https://example.com").await; assert!(result.is_ok()); } // Multi-thread runtime (matches production tokio::main) #[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn concurrent_requests() { let (r1, r2) = tokio::join!( do_request("a"), do_request("b"), ); assert!(r1.is_ok()); assert!(r2.is_ok()); } // Current-thread runtime (deterministic, good for unit tests) #[tokio::test(flavor = "current_thread")] async fn sequential_processing() { let result = process_sequentially(vec![1, 2, 3]).await; assert_eq!(result, vec![2, 4, 6]); } ``` ### Mock Time with `tokio::time::pause` ```rust use tokio::time::{self, Duration, Instant}; #[tokio::test] async fn cache_expires_after_ttl() { time::pause(); // freeze the clock let cache = Cache::with_ttl(Duration::from_secs(60)); cache.insert("key", "value"); assert_eq!(cache.get("key"), Some("value")); time::advance(Duration::from_secs(61)).await; // advance clock assert_eq!(cache.get("key"), None); // now expired } ``` --- ## 5. mockall ```toml mockall = "0.12" ``` ### Automock a Trait ```rust use mockall::automock; #[automock] pub trait UserRepository: Send + Sync { async fn find_by_id(&self, id: u64) -> Option<User>; async fn save(&self, user: &User) -> Result<(), DbError>; fn count(&self) -> usize; } ``` ### Configure Expectations in Tests ```rust #[cfg(test)] mod tests { use super::*; use mockall::predicate::*; #[tokio::test] async fn get_user_returns_user_when_found() { let mut mock = MockUserRepository::new(); mock.expect_find_by_id() .with(eq(42u64)) // match specific argument .times(1) // must be called exactly once .returning(|_| Some(User { id: 42, name: "Alice".to_string() })); let service = UserService::new(mock); let user = service.get_user(42).await.unwrap(); assert_eq!(user.name, "Alice"); } #[tokio::test] async fn get_user_returns_error_when_not_found() { let mut mock = MockUserRepository::new(); mock.expect_find_by_id() .returning(|_| None); // any argument, always None let service = UserService::new(mock); let result = service.get_user(99).await; assert!(matches!(result, Err(ServiceError::NotFound))); } #[test] fn saves_only_valid_users() { let mut mock = MockUserRepository::new(); mock.expect_save() .withf(|user| !user.name.is_empty()) // custom predicate .times(1) .returning(|_| Ok(())); // mock verifies expectations on drop } } ``` ### Chain Sequences of Calls ```rust use mockall::Sequence; #[test] fn retries_on_first_failure() { let mut mock = MockUserRepository::new(); let mut seq = Sequence::new(); mock.expect_count() .times(1) .in_sequence(&mut seq) .returning(|| 0); mock.expect_count() .times(1) .in_sequence(&mut seq) .returning(|| 5); assert_eq!(mock.count(), 0); assert_eq!(mock.count(), 5); } ``` ### Mock Structs (not just traits) ```rust use mockall::mock; mock! { pub HttpClient { pub fn get(&self, url: &str) -> Result<String, reqwest::Error>; pub fn post(&self, url: &str, body: &str) -> Result<String, reqwest::Error>; } } ``` --- ## 6. Test Fixtures ### Set Up and Tear Down with Drop ```rust pub struct TestDb { pub pool: sqlx::PgPool, pub db_name: String, } impl TestDb { pub async fn new() -> Self { let db_name = format!("test_{}", uuid::Uuid::new_v4().simple()); let admin_pool = sqlx::PgPool::connect("postgres://localhost/postgres").await.unwrap(); sqlx::query(&format!("CREATE DATABASE {}", db_name)) .execute(&admin_pool) .await .unwrap(); let pool = sqlx::PgPool::connect(&format!("postgres://localhost/{}", db_name)) .await .unwrap(); sqlx::migrate!("./migrations").run(&pool).await.unwrap(); TestDb { pool, db_name } } } impl Drop for TestDb { fn drop(&mut self) { // Schedule async cleanup - use a blocking approach here let db_name = self.db_name.clone(); std::thread::spawn(move || { let rt = tokio::runtime::Runtime::new().unwrap(); rt.block_on(async { let pool = sqlx::PgPool::connect("postgres://localhost/postgres").await.unwrap(); sqlx::query(&format!("DROP DATABASE IF EXISTS {}", db_name)) .execute(&pool) .await .ok(); }); }); } } ``` ### Share Expensive Setup with `OnceLock` ```rust use std::sync::OnceLock; static CONFIG: OnceLock<TestConfig> = OnceLock::new(); fn test_config() -> &'static TestConfig { CONFIG.get_or_init(|| TestConfig::load_from_env()) } #[test] fn uses_shared_config() { let config = test_config(); assert!(!config.api_key.is_empty()); } ``` ### Use Temporary Directories ```rust use tempfile::TempDir; #[test] fn writes_output_file() { let dir = TempDir::new().unwrap(); // deleted on drop let file_path = dir.path().join("output.txt"); write_results(&file_path, &[1, 2, 3]).unwrap(); let contents = std::fs::read_to_string(&file_path).unwrap(); assert!(contents.contains("1")); } // Keep dir alive for the test scope #[test] fn reads_fixture_from_temp() { let dir = TempDir::new().unwrap(); std::fs::write(dir.path().join("input.json"), br#"{"key":"value"}"#).unwrap(); let result = process_file(dir.path().join("input.json")).unwrap(); assert_eq!(result.get("key").unwrap(), "value"); // dir dropped here, cleanup happens } ``` --- ## 7. Property-Based Testing ```toml proptest = "1" ``` ### Write Property Tests ```rust use proptest::prelude::*; proptest! { #[test] fn parse_then_serialize_roundtrips(s in "[a-zA-Z0-9]{1,20}") { let parsed = parse_identifier(&s).unwrap(); let serialized = serialize_identifier(&parsed); prop_assert_eq!(s, serialized); } #[test] fn sort_is_idempotent(mut v in prop::collection::vec(any::<i32>(), 0..100)) { v.sort(); let sorted_once = v.clone(); v.sort(); prop_assert_eq!(sorted_once, v); } #[test] fn addition_commutes(a in 0i32..1000, b in 0i32..1000) { prop_assert_eq!(a + b, b + a); } } ``` ### Derive `Arbitrary` for Custom Types ```rust use proptest_derive::Arbitrary; #[derive(Debug, Clone, Arbitrary)] pub struct User { #[proptest(regex = "[a-z]{3,20}")] pub username: String, pub age: u8, pub active: bool, } proptest! { #[test] fn user_validation_never_panics(user in any::<User>()) { // Should return Ok or Err, never panic let _ = validate_user(&user); } } ``` ### Handle Shrinking and Regression Files Proptest automatically saves failing inputs to `proptest-regressions/` and replays them on subsequent runs. Commit these files to catch regressions. Suppress with `#[proptest(skip_shrink)]` for expensive types. --- ## 8. Benchmarks ```toml [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } [[bench]] name = "my_bench" harness = false ``` ### Write Criterion Benchmarks ```rust // benches/my_bench.rs use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion, Throughput}; use my_crate::{parse, process}; fn bench_parse(c: &mut Criterion) { let input = "example input string"; c.bench_function("parse_simple", |b| { b.iter(|| parse(criterion::black_box(input))) }); } fn bench_process_sizes(c: &mut Criterion) { let mut group = c.benchmark_group("process"); for size in [100usize, 1_000, 10_000] { let data: Vec<u8> = (0..size).map(|i| i as u8).collect(); group.throughput(Throughput::Bytes(size as u64)); group.bench_with_input(BenchmarkId::from_parameter(size), &data, |b, data| { b.iter(|| process(criterion::black_box(data))) }); } group.finish(); } fn bench_comparison(c: &mut Criterion) { let mut group = c.benchmark_group("sort_comparison"); let data: Vec<i32> = (0..1000).rev().collect(); group.bench_function("std_sort", |b| { b.iter(|| { let mut v = data.clone(); v.sort(); v }) }); group.bench_function("unstable_sort", |b| { b.iter(|| { let mut v = data.clone(); v.sort_unstable(); v }) }); group.finish(); } criterion_group!(benches, bench_parse, bench_process_sizes, bench_comparison); criterion_main!(benches); ``` ### Run Benchmarks and Generate Flamegraphs ```bash # Run all benchmarks cargo bench # Run specific benchmark cargo bench --bench my_bench parse # Save baseline for comparison cargo bench -- --save-baseline before # ... make changes ... cargo bench -- --baseline before # Generate flamegraph (requires cargo-flamegraph and perf/dtrace) cargo flamegraph --bench my_bench -- --bench bench_parse ``` --- ## 9. Snapshot Testing ```toml insta = { version = "1", features = ["json", "yaml", "redactions"] } ``` ### Assert with Snapshots ```rust use insta::assert_snapshot; #[test] fn renders_report() { let report = generate_report(&sample_data()); assert_snapshot!(report); // First run: creates snapshot file in snapshots/ directory // Subsequent runs: compares against saved snapshot } // JSON snapshots (pretty-printed, sorted keys) use insta::assert_json_snapshot; #[test] fn serializes_user() { let user = User { id: 1, name: "Alice".into(), active: true }; assert_json_snapshot!(user); } ``` ### Use Redactions for Dynamic Values ```rust use insta::assert_json_snapshot; #[test] fn snapshot_with_dynamic_id() { let response = create_item("test"); assert_json_snapshot!(response, { ".id" => "[id]", // replace dynamic id ".created_at" => "[timestamp]", // replace timestamp }); } ``` ### Review and Accept Snapshots ```bash # Install the review tool cargo install cargo-insta # Run tests (failures create .snap.new files) cargo test # Review all pending snapshots interactively cargo insta review # Accept all pending snapshots at once cargo insta accept ``` Commit `.snap` files alongside code. They are the expected output and act as documentation. --- ## 10. Test Organization ### Build a Common Test Utilities Module ``` tests/ ├── common/ │ ├── mod.rs # re-exports all helpers │ ├── fixtures.rs # load JSON/TOML test data │ ├── builders.rs # test builder patterns for structs │ └── assertions.rs # custom assert helpers ``` ```rust // tests/common/builders.rs pub struct UserBuilder { id: u64, name: String, email: String, } impl UserBuilder { pub fn new() -> Self { UserBuilder { id: 1, name: "Test User".into(), email: "test@example.com".into() } } pub fn id(mut self, id: u64) -> Self { self.id = id; self } pub fn name(mut self, name: impl Into<String>) -> Self { self.name = name.into(); self } pub fn build(self) -> User { User { id: self.id, name: self.name, email: self.email } } } ``` ### Extract a `test-utils` Workspace Crate For large workspaces, extract test utilities into a dedicated crate: ```toml # Cargo.toml (workspace root) [workspace] members = ["my-app", "my-lib", "test-utils"] # my-lib/Cargo.toml [dev-dependencies] test-utils = { path = "../test-utils" } ``` This avoids duplicating helpers across crates and allows `#[cfg(test)]`-gated re-exports. ### Write Custom Assertion Helpers ```rust // tests/common/assertions.rs pub fn assert_sorted<T: Ord + std::fmt::Debug>(items: &[T]) { for window in items.windows(2) { assert!( window[0] <= window[1], "Expected sorted slice, found {:?} before {:?}", window[0], window[1] ); } } pub fn assert_error_contains(result: &anyhow::Result<()>, expected: &str) { match result { Err(e) => assert!( e.to_string().contains(expected), "Expected error to contain '{}', got: {}", expected, e ), Ok(_) => panic!("Expected error containing '{}', got Ok", expected), } } ``` --- ## 11. CI Patterns ### Run the Full Test Suite ```yaml # .github/workflows/ci.yml name: CI on: [push, pull_request] jobs: test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable with: components: clippy, rustfmt - uses: Swatinem/rust-cache@v2 - name: Format check run: cargo fmt --all -- --check - name: Lint run: cargo clippy --all-targets --all-features -- -D warnings - name: Test run: cargo test --workspace --all-features env: DATABASE_URL: postgres://postgres:postgres@localhost/test - name: Doc test run: cargo test --doc --workspace ``` ### Test a Feature Matrix ```yaml strategy: matrix: features: ["", "feature-a", "feature-b", "full"] steps: - name: Test feature set run: cargo test --no-default-features --features "${{ matrix.features }}" ``` ### Measure Coverage with `cargo-llvm-cov` ```bash # Install cargo install cargo-llvm-cov # Generate coverage report cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info # HTML report locally cargo llvm-cov --workspace --html open target/llvm-cov/html/index.html ``` ```yaml # In CI - name: Coverage run: cargo llvm-cov --workspace --all-features --lcov --output-path lcov.info - uses: codecov/codecov-action@v4 with: files: lcov.info ``` ### Run Tests Against a Live Database in CI ```yaml services: postgres: image: postgres:16 env: POSTGRES_PASSWORD: postgres POSTGRES_DB: test ports: - 5432:5432 options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ``` ### Check for Unused Dependencies ```bash cargo install cargo-machete cargo machete # Or for dependency audit cargo install cargo-audit cargo audit ``` ### Enforce MSRV (Minimum Supported Rust Version) ```toml # Cargo.toml [package] rust-version = "1.75" ``` ```yaml - uses: dtolnay/rust-toolchain@1.75 - run: cargo test --workspace ``` -
traits-generics.md 16.5 KB
# Traits and Generics Reference ## Table of Contents 1. [Trait Definition](#1-trait-definition) 2. [Trait Bounds](#2-trait-bounds) 3. [Associated Types vs Generic Parameters](#3-associated-types-vs-generic-parameters) 4. [Supertraits](#4-supertraits) 5. [Trait Objects](#5-trait-objects) 6. [Derive Macros](#6-derive-macros) 7. [Common Trait Implementations](#7-common-trait-implementations) 8. [Sealed Traits](#8-sealed-traits) 9. [Extension Traits](#9-extension-traits) 10. [Generics](#10-generics) 11. [Blanket Implementations](#11-blanket-implementations) --- ## 1. Trait Definition ### Define Methods, Default Implementations, and Associated Functions ```rust pub trait Greet { // Required method — implementors must provide this fn name(&self) -> &str; // Default method — implementors may override fn greeting(&self) -> String { format!("Hello, {}!", self.name()) } // Associated function (no self) — often used as constructors fn kind() -> &'static str { "greeter" } } struct Person { name: String, } impl Greet for Person { fn name(&self) -> &str { &self.name } // greeting() uses the default implementation } let p = Person { name: "Alice".into() }; println!("{}", p.greeting()); // "Hello, Alice!" println!("{}", Person::kind()); // "greeter" ``` ### Define Traits with Associated Types and Constants ```rust pub trait Encode { type Output; const VERSION: u8 = 1; fn encode(&self) -> Self::Output; } struct Json(String); impl Encode for Json { type Output = Vec<u8>; const VERSION: u8 = 2; // override default fn encode(&self) -> Vec<u8> { self.0.as_bytes().to_vec() } } ``` --- ## 2. Trait Bounds ### Apply Bounds to Functions ```rust // Inline bound fn print_item<T: std::fmt::Display>(item: T) { println!("{}", item); } // Where clause — cleaner for multiple or complex bounds fn log<T, E>(result: Result<T, E>) where T: std::fmt::Debug, E: std::fmt::Display, { match result { Ok(v) => println!("OK: {:?}", v), Err(e) => eprintln!("ERR: {}", e), } } // Multiple bounds with + fn serialize_and_print<T: serde::Serialize + std::fmt::Debug>(val: &T) { println!("{:?}", val); let json = serde_json::to_string(val).unwrap(); println!("{}", json); } ``` ### Use impl Trait in Argument Position `impl Trait` in argument position is syntactic sugar for a generic parameter with that bound. Each call site can use a different concrete type. ```rust // These are equivalent fn process(item: impl std::fmt::Display) { println!("{}", item); } fn process<T: std::fmt::Display>(item: T) { println!("{}", item); } // impl Trait in return position — hides the concrete type fn make_adder(x: i32) -> impl Fn(i32) -> i32 { move |y| x + y } ``` Note: `impl Trait` in return position always returns the same concrete type — it is not a trait object. Use `Box<dyn Trait>` when you need to return different types. ### Apply Bounds to Structs and Impls ```rust struct Wrapper<T: Clone> { val: T, } // Bound on impl — methods only available when T: Clone + std::fmt::Debug impl<T: Clone + std::fmt::Debug> Wrapper<T> { fn inspect(&self) -> T { println!("{:?}", self.val); self.val.clone() } } ``` --- ## 3. Associated Types vs Generic Parameters ### Choose Associated Types for One-to-One Relationships Use associated types when there is only one sensible implementation per type. `Iterator` is the canonical example — a type can only produce one kind of item. ```rust // Associated type: Vec<i32> implements Iterator<Item = &i32> // There is exactly one Item type per implementor trait Iterator { type Item; fn next(&mut self) -> Option<Self::Item>; } // Caller syntax is clean fn sum_iter<I: Iterator<Item = i32>>(mut it: I) -> i32 { let mut total = 0; while let Some(n) = it.next() { total += n; } total } ``` ### Choose Generic Parameters for Multiple Implementations Use generic parameters when a type may implement the trait for many different type arguments. `From<T>` is the canonical example — `String` implements `From<&str>`, `From<char>`, etc. ```rust // Generic parameter: String can implement Converter for many T trait Converter<T> { fn convert(&self) -> T; } struct Celsius(f64); impl Converter<f64> for Celsius { fn convert(&self) -> f64 { self.0 } } impl Converter<String> for Celsius { fn convert(&self) -> String { format!("{}°C", self.0) } } let c = Celsius(100.0); let f: f64 = c.convert(); let s: String = c.convert(); ``` --- ## 4. Supertraits ### Require Other Traits A supertrait is a trait that must be implemented before another trait can be implemented. Declare it with `:` after the trait name. ```rust use std::fmt; // Animal requires Display and Debug trait Animal: fmt::Display + fmt::Debug { fn sound(&self) -> &str; } #[derive(Debug)] struct Dog; impl fmt::Display for Dog { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Dog") } } impl Animal for Dog { fn sound(&self) -> &str { "woof" } } ``` ### Coerce to a Supertrait Object You can use a trait object of the supertrait when you only need shared functionality. ```rust fn describe(animal: &dyn fmt::Display) { println!("{}", animal); } let dog = Dog; describe(&dog as &dyn fmt::Display); ``` --- ## 5. Trait Objects ### Create and Use dyn Trait A trait object (`dyn Trait`) is a fat pointer: a data pointer plus a vtable pointer. They enable dynamic dispatch. ```rust trait Shape { fn area(&self) -> f64; } struct Circle { radius: f64 } struct Square { side: f64 } impl Shape for Circle { fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius } } impl Shape for Square { fn area(&self) -> f64 { self.side * self.side } } // Heterogeneous collection via Box<dyn Trait> let shapes: Vec<Box<dyn Shape>> = vec![ Box::new(Circle { radius: 1.0 }), Box::new(Square { side: 2.0 }), ]; for shape in &shapes { println!("area = {:.2}", shape.area()); } ``` ### Satisfy Object Safety Rules A trait is object-safe (usable as `dyn Trait`) if: - It has no methods that return `Self` - It has no generic methods - All methods are dispatchable (take `&self`, `&mut self`, or `Box<Self>`) ```rust // NOT object-safe: clone() returns Self // trait Cloneable: Clone {} // cannot be dyn // Object-safe version: return Box<dyn Trait> trait DynClone { fn clone_box(&self) -> Box<dyn DynClone>; } // NOT object-safe: generic method trait Bad { fn convert<T>(&self) -> T; // generic method — not dispatchable } // OK: use associated type instead trait Good { type Output; fn convert(&self) -> Self::Output; } ``` ### Add Send + Sync to Trait Objects for Threads ```rust // Sendable trait object fn spawn_worker(task: Box<dyn Fn() + Send + 'static>) { std::thread::spawn(task); } // Arc<dyn Trait + Send + Sync> for shared access across threads use std::sync::Arc; let shared: Arc<dyn Shape + Send + Sync> = Arc::new(Circle { radius: 1.0 }); ``` --- ## 6. Derive Macros ### Use Common Derives ```rust #[derive(Debug, Clone, PartialEq, Eq, Hash, Default)] struct Config { name: String, value: u32, } // Debug: {:?} and {:#?} formatting // Clone: .clone() method // PartialEq/Eq: == and != operators // Hash: usable as HashMap/HashSet key (requires PartialEq + Eq) // Default: Config::default() returns Config { name: "", value: 0 } #[derive(PartialOrd, Ord, PartialEq, Eq)] struct Version(u32, u32, u32); // PartialOrd/Ord: <, >, <=, >= operators; enables .sort() on Vec<Version> // Ord requires PartialOrd; PartialOrd requires PartialEq ``` ### Extend with the derive_more Crate `derive_more` provides derives for common trait impls that the standard library does not include. ```rust // Cargo.toml: derive_more = { version = "1", features = ["display", "from", "into"] } use derive_more::{Display, From, Into}; #[derive(Display, From, Into)] #[display("User({name}, {id})")] struct UserId { name: String, id: u64, } let id = UserId::from(("Alice".to_string(), 42u64)); let (name, num): (String, u64) = id.into(); ``` --- ## 7. Common Trait Implementations ### Implement Display ```rust use std::fmt; struct Point { x: f64, y: f64 } impl fmt::Display for Point { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "({:.2}, {:.2})", self.x, self.y) } } // Implementing Display gives .to_string() for free via blanket impl let p = Point { x: 1.0, y: 2.0 }; println!("{}", p); let s: String = p.to_string(); ``` ### Implement From and Into Implement `From`; `Into` is derived automatically via a blanket impl. ```rust struct Meters(f64); struct Feet(f64); impl From<Meters> for Feet { fn from(m: Meters) -> Self { Feet(m.0 * 3.28084) } } let m = Meters(1.0); let f: Feet = m.into(); // Into<Feet> for Meters — derived from From let f2 = Feet::from(Meters(2.0)); // From<Meters> for Feet ``` ### Implement FromStr ```rust use std::str::FromStr; #[derive(Debug)] struct Color { r: u8, g: u8, b: u8 } #[derive(Debug)] struct ParseColorError(String); impl std::fmt::Display for ParseColorError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "parse color error: {}", self.0) } } impl FromStr for Color { type Err = ParseColorError; fn from_str(s: &str) -> Result<Self, Self::Err> { let parts: Vec<&str> = s.split(',').collect(); if parts.len() != 3 { return Err(ParseColorError("expected R,G,B".into())); } let parse = |p: &str| p.trim().parse::<u8>() .map_err(|e| ParseColorError(e.to_string())); Ok(Color { r: parse(parts[0])?, g: parse(parts[1])?, b: parse(parts[2])? }) } } let c: Color = "255, 128, 0".parse().unwrap(); ``` ### Implement Deref and DerefMut `Deref` enables the `*` operator and auto-deref coercions. Use it for smart-pointer-like types, not general type conversions. ```rust use std::ops::{Deref, DerefMut}; struct Wrapper<T>(Vec<T>); impl<T> Deref for Wrapper<T> { type Target = Vec<T>; fn deref(&self) -> &Vec<T> { &self.0 } } impl<T> DerefMut for Wrapper<T> { fn deref_mut(&mut self) -> &mut Vec<T> { &mut self.0 } } let mut w = Wrapper(vec![1, 2, 3]); w.push(4); // DerefMut: Vec::push via auto-deref println!("{}", w.len()); // Deref: Vec::len via auto-deref ``` ### Implement AsRef and AsMut `AsRef<T>` is for cheap reference conversions. Prefer it over `Deref` in function parameters. ```rust // Accept String, &str, PathBuf, &Path, etc. — anything that is AsRef<str> fn print_upper(s: impl AsRef<str>) { println!("{}", s.as_ref().to_uppercase()); } print_upper("hello"); print_upper(String::from("world")); // AsRef<Path> for filesystem functions fn read_config(path: impl AsRef<std::path::Path>) -> std::io::Result<String> { std::fs::read_to_string(path) } ``` --- ## 8. Sealed Traits ### Prevent External Implementations The sealed trait pattern restricts who can implement a trait — useful for stable API surfaces in libraries. ```rust // In your library crate mod private { pub trait Sealed {} } pub trait MyTrait: private::Sealed { fn do_thing(&self); } // Implement Sealed only for types you control pub struct TypeA; pub struct TypeB; impl private::Sealed for TypeA {} impl private::Sealed for TypeB {} impl MyTrait for TypeA { fn do_thing(&self) { println!("A"); } } impl MyTrait for TypeB { fn do_thing(&self) { println!("B"); } } // External users CANNOT implement MyTrait because they cannot implement // private::Sealed (it's not publicly accessible) ``` --- ## 9. Extension Traits ### Add Methods to Foreign Types Extension traits let you add methods to types you do not own, including primitives and standard library types. ```rust pub trait StrExt { fn word_count(&self) -> usize; fn capitalize(&self) -> String; } impl StrExt for str { fn word_count(&self) -> usize { self.split_whitespace().count() } fn capitalize(&self) -> String { let mut chars = self.chars(); match chars.next() { None => String::new(), Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(), } } } // Bring the trait into scope to use the methods use crate::StrExt; println!("{}", "hello world".word_count()); // 2 println!("{}", "hello".capitalize()); // "Hello" ``` Extension traits must be in scope (imported) to use their methods. This is why `use std::io::Write` and similar imports are necessary. --- ## 10. Generics ### Use Type Parameters ```rust // Generic struct struct Stack<T> { items: Vec<T>, } impl<T> Stack<T> { fn push(&mut self, item: T) { self.items.push(item); } fn pop(&mut self) -> Option<T> { self.items.pop() } fn is_empty(&self) -> bool { self.items.is_empty() } } // Generic enum enum Either<L, R> { Left(L), Right(R), } ``` ### Use Const Generics Const generics allow types to be parameterized by constant values (integers, booleans, chars). ```rust // Array type parameterized by size — zero-cost abstraction struct Matrix<T, const ROWS: usize, const COLS: usize> { data: [[T; COLS]; ROWS], } impl<T: Default + Copy, const R: usize, const C: usize> Matrix<T, R, C> { fn new() -> Self { Matrix { data: [[T::default(); C]; R] } } fn rows(&self) -> usize { R } fn cols(&self) -> usize { C } } let m: Matrix<f64, 3, 4> = Matrix::new(); assert_eq!(m.rows(), 3); ``` ### Use PhantomData for Marker Types `PhantomData<T>` tells the compiler that a type logically contains `T` without storing it, affecting variance and drop checking. ```rust use std::marker::PhantomData; // A typed ID that cannot be mixed between entity types struct Id<T> { value: u64, _phantom: PhantomData<T>, } impl<T> Id<T> { fn new(value: u64) -> Self { Id { value, _phantom: PhantomData } } } struct User; struct Order; let user_id: Id<User> = Id::new(1); let order_id: Id<Order> = Id::new(1); // Cannot mix: user_id and order_id are different types even though value is same ``` ### Use the Turbofish Syntax When the compiler cannot infer a generic type argument, use `::<>` (turbofish) to supply it explicitly. ```rust // Collect requires knowing what to collect into let nums: Vec<i32> = "1 2 3".split(' ') .map(|s| s.parse::<i32>().unwrap()) // turbofish on parse .collect::<Vec<_>>(); // turbofish on collect (alternative to type annotation) // Any generic function may need turbofish fn identity<T>(val: T) -> T { val } let x = identity::<String>(String::from("hello")); ``` ### Express Lifetime Constraints on Generic Types `T: 'a` means "all references inside T live at least as long as 'a". This is required when storing generic types behind references. ```rust struct Holder<'a, T: 'a> { reference: &'a T, } // 'static bound: T contains no non-static references // (common for thread-spawning and stored callbacks) fn store_callback<F: Fn() + Send + 'static>(f: F) { std::thread::spawn(f); } ``` --- ## 11. Blanket Implementations ### Understand Blanket Impls A blanket implementation applies a trait to any type that satisfies certain bounds, rather than to a specific named type. ```rust // From the standard library — any T that implements Display also gets ToString impl<T: std::fmt::Display> ToString for T { fn to_string(&self) -> String { format!("{}", self) } } // This is why any Display type has .to_string() for free 42i32.to_string(); 3.14f64.to_string(); ``` ### Write Blanket Impls for Your Own Traits ```rust trait Summary { fn summarize(&self) -> String; } // Any type that implements Display also gets a free Summary impl impl<T: std::fmt::Display> Summary for T { fn summarize(&self) -> String { format!("Summary: {}", self) } } ``` Be careful: blanket impls can create conflicts. Two blanket impls that could overlap will fail to compile (the orphan rule plus coherence checking prevents ambiguity). ### Apply the Orphan Rule You can implement a trait for a type only if either the trait or the type is defined in your crate. Both cannot be foreign. ```rust // OK: MyTrait (yours) for String (foreign) impl MyTrait for String { ... } // OK: Display (foreign) for MyType (yours) impl std::fmt::Display for MyType { ... } // ERROR: Display (foreign) for Vec<T> (foreign) — orphan rule violation // impl std::fmt::Display for Vec<i32> { ... } ``` Work around this using the newtype pattern: ```rust struct MyVec(Vec<i32>); impl std::fmt::Display for MyVec { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self.0) } } ```
-
-
scripts
-
check-rust-facts.py 11.3 KB
#!/usr/bin/env python3 """Staleness verifier for rust-ops: the version-bearing facts the skill encodes must stay real and cited. rust-ops anchors its currency to a few version-bearing facts — the major versions of its core ecosystem crates (tokio, axum, serde). That is exactly the fact that drifts silently (SKILL-RESOURCE-PROTOCOL.md §7): a crate leaves its documented major upstream (axum 0.x → 1.0, say), or the prose stops naming a crate the catalog still commits to, and nobody notices for months. Two modes guard it: --offline (default, safe for PR CI): structural consistency, no network. * assets/rust-facts.json parses; every entry has name + documented_major * every catalogued crate is still named somewhere in the skill prose (SKILL.md / references/*.md) — the catalog can't drift from the docs * SKILL.md still carries a dated "as of 20XX" currency note --live (scheduled freshness.yml, never a PR gate): does each crate's latest stable major on crates.io still match the documented major? A newer major = the skill is behind reality (drift). Exit 7 if crates.io is unreachable. (crates.io requires a User-Agent header or it 403s.) Usage: check-rust-facts.py [--offline | --live] [--catalog FILE] [--skill DIR] [--json] [--timeout S] Input: argv flags only (no stdin). Output: stdout = findings (plain rows, or a --json envelope). Data only. Stderr: the verdict line, notices, errors. Exit: 0 ok, 2 usage, 3 catalog/skill missing, 4 catalog unparseable, 7 crates.io unreachable (live, advisory — never a real failure), 10 drift found (offline: uncited/undocumented/no currency note; live: published major newer than documented major) Examples: check-rust-facts.py --offline # PR CI: catalog ⇆ prose consistency check-rust-facts.py --live # weekly: every crate's major still matches crates.io check-rust-facts.py --offline --json | jq '.data[]' """ from __future__ import annotations import argparse import json import os import re import sys import urllib.error import urllib.parse import urllib.request from pathlib import Path EX_OK = 0 EX_USAGE = 2 EX_NOTFOUND = 3 EX_UNPARSEABLE = 4 EX_UNAVAILABLE = 7 EX_DRIFT = 10 HERE = Path(__file__).resolve().parent DEFAULT_CATALOG = HERE.parent / "assets" / "rust-facts.json" DEFAULT_SKILL = HERE.parent DEFAULT_REGISTRY = "https://crates.io/api/v1/crates" SCHEMA = "claude-mods.rust-ops.facts/v1" CURRENCY_RE = re.compile(r"as of 20\d\d") def eprint(*a) -> None: print(*a, file=sys.stderr) class Term: """Minimal ANSI helper (term.sh is bash-only; per TERMINAL-DESIGN.md §9 the Python port is inline). Honors FORCE_COLOR / NO_COLOR / TERM_ASCII and the bound stream's TTY + encoding so piped data stays plain ASCII.""" _C = {"green": "\033[32m", "red": "\033[31m", "dim": "\033[2m", "off": "\033[0m"} def __init__(self, stream=sys.stderr) -> None: enc = (getattr(stream, "encoding", "") or "").lower() self.ascii = os.environ.get("TERM_ASCII") == "1" or "utf" not in enc if os.environ.get("FORCE_COLOR"): self.color = True elif (os.environ.get("NO_COLOR") is not None or os.environ.get("TERM") == "dumb" or not getattr(stream, "isatty", lambda: False)()): self.color = False else: self.color = True def c(self, name: str, text: str) -> str: return f"{self._C.get(name, '')}{text}{self._C['off']}" if self.color else text def mark(self, ok: bool) -> str: g = ("+" if self.ascii else "✓") if ok else ("x" if self.ascii else "✗") return self.c("green" if ok else "red", g) def load_catalog(path: Path) -> tuple[list[dict], str]: """Returns (packages, registry). Each package has name + documented_major.""" if not path.is_file(): eprint(f"error: package catalog not found: {path}") raise SystemExit(EX_NOTFOUND) try: data = json.loads(path.read_text(encoding="utf-8")) pkgs = data["packages"] if not isinstance(pkgs, list) or not pkgs: raise ValueError("'packages' must be a non-empty array") for p in pkgs: if not isinstance(p, dict) or "name" not in p or "documented_major" not in p: raise ValueError(f"package entry missing name/documented_major: {p!r}") dm = p["documented_major"] if not isinstance(dm, int) or isinstance(dm, bool) or dm < 0: raise ValueError(f"documented_major must be a non-negative int: {p!r}") registry = data.get("registry") or DEFAULT_REGISTRY return pkgs, registry except (json.JSONDecodeError, KeyError, TypeError, ValueError) as exc: eprint(f"error: could not parse catalog {path}: {exc}") raise SystemExit(EX_UNPARSEABLE) def read_corpus(skill_dir: Path) -> tuple[str, str]: """Returns (skill_md_text, all_prose_text) across SKILL.md + references/*.md.""" doc = skill_dir / "SKILL.md" if not doc.is_file(): eprint(f"error: SKILL.md not found under {skill_dir}") raise SystemExit(EX_NOTFOUND) skill_md = doc.read_text(encoding="utf-8") parts = [skill_md] for ref in sorted((skill_dir / "references").glob("*.md")): parts.append(ref.read_text(encoding="utf-8")) return skill_md, "\n".join(parts) def check_offline(pkgs: list[dict], skill_dir: Path) -> list[dict]: skill_md, corpus = read_corpus(skill_dir) findings: list[dict] = [] for p in pkgs: name = p["name"] # case-sensitive exact substring: crate names are case-sensitive (serde != Serde) if name not in corpus: findings.append({"package": name, "issue": "catalogued but not named in skill prose"}) if not CURRENCY_RE.search(skill_md): findings.append({"package": "(SKILL.md)", "issue": "no dated 'as of 20XX' currency note"}) return findings def crates_latest(registry: str, name: str, timeout: float) -> tuple[str, object]: """Return ('ok', version_str) | ('gone', None) | ('unreachable', info). crates.io 403s requests without a User-Agent header, so one is mandatory.""" url = registry.rstrip("/") + "/" + urllib.parse.quote(name, safe="") req = urllib.request.Request(url, method="GET", headers={"User-Agent": "claude-mods-rust-ops-check/1", "Accept": "application/json"}) try: with urllib.request.urlopen(req, timeout=timeout) as resp: data = json.loads(resp.read().decode("utf-8")) crate = data.get("crate", {}) if isinstance(data, dict) else {} # max_stable_version skips pre-releases (0.8.0-beta.1); fall back to max_version. ver = crate.get("max_stable_version") or crate.get("max_version", "") return ("ok", ver) except urllib.error.HTTPError as exc: if exc.code in (404, 410): return ("gone", None) return ("unreachable", exc.code) # 403/5xx etc: transient or auth, not a content finding except (urllib.error.URLError, TimeoutError, OSError, json.JSONDecodeError) as exc: return ("unreachable", str(getattr(exc, "reason", exc))) def major_of(version: str) -> int | None: m = re.match(r"\D*(\d+)", version or "") return int(m.group(1)) if m else None def check_live(pkgs: list[dict], registry: str, timeout: float) -> tuple[list[dict], list[dict]]: drift: list[dict] = [] unreachable: list[dict] = [] for p in pkgs: name = p["name"] doc = p["documented_major"] status, info = crates_latest(registry, name, timeout) if status == "gone": drift.append({"package": name, "issue": "no longer resolves on crates.io (404)"}) elif status != "ok": unreachable.append({"package": name, "issue": f"unreachable: {info}"}) else: live_major = major_of(str(info)) if live_major is None: unreachable.append({"package": name, "issue": f"could not parse version {info!r}"}) elif live_major > doc: drift.append({"package": name, "issue": f"crates.io@{info} major {live_major} > documented major {doc}"}) return drift, unreachable def main(argv: list[str]) -> int: p = argparse.ArgumentParser( prog="check-rust-facts.py", description="Verify rust-ops' version-bearing facts stay cited (offline) and current on crates.io (live).", ) mode = p.add_mutually_exclusive_group() mode.add_argument("--offline", action="store_true", help="structural consistency, no network (default)") mode.add_argument("--live", action="store_true", help="check every crate's latest major still matches crates.io") p.add_argument("--catalog", default=str(DEFAULT_CATALOG), help="facts catalog JSON") p.add_argument("--skill", default=str(DEFAULT_SKILL), help="skill directory (SKILL.md + references/)") p.add_argument("--timeout", type=float, default=10.0, help="per-request timeout seconds (live)") p.add_argument("--json", action="store_true", help="emit a JSON envelope") try: args = p.parse_args(argv) except SystemExit as exc: return EX_USAGE if exc.code not in (0, None) else (exc.code or EX_OK) pkgs, registry = load_catalog(Path(args.catalog)) live = args.live and not args.offline t = Term(sys.stderr) if live: drift, unreachable = check_live(pkgs, registry, args.timeout) findings = drift + unreachable if args.json: print(json.dumps({ "data": findings, "meta": {"mode": "live", "packages_checked": len(pkgs), "drift": len(drift), "unreachable": len(unreachable), "registry": registry, "schema": SCHEMA}, }, indent=2)) else: for f in drift: print(f"DRIFT {f['package']}: {f['issue']}") for f in unreachable: print(f"UNREACH {f['package']}: {f['issue']}") # §7: confirmed drift -> 10; else transient/unreachable -> 7 (advisory); else 0. if drift: eprint(f"{t.mark(False)} rust-facts/live: {len(drift)} crate(s) drifted from documented major " f"{t.c('dim', '(' + registry + ')')}") return EX_DRIFT if unreachable: eprint(f"{t.mark(False)} rust-facts/live: crates.io unreachable for " f"{len(unreachable)}/{len(pkgs)} {t.c('dim', '(advisory - retry next run)')}") return EX_UNAVAILABLE eprint(f"{t.mark(True)} rust-facts/live: all {len(pkgs)} crate(s) match documented major on crates.io") return EX_OK # offline (default) findings = check_offline(pkgs, Path(args.skill)) if args.json: print(json.dumps({ "data": findings, "meta": {"mode": "offline", "packages_checked": len(pkgs), "drift": len(findings), "consistent": not findings, "schema": SCHEMA}, }, indent=2)) else: for f in findings: print(f"DRIFT {f['package']}: {f['issue']}") ok = not findings eprint(f"{t.mark(ok)} rust-facts/offline: {len(pkgs)} crate(s) checked, " f"{len(findings)} inconsistency {t.c('dim', '(catalog vs skill prose)')}") return EX_DRIFT if findings else EX_OK if __name__ == "__main__": sys.exit(main(sys.argv[1:]))
-
-
tests
-
run.sh 5.1 KB
#!/usr/bin/env bash # Self-test for the rust-ops skill. # # Offline-deterministic (no network, no Rust toolchain required). Asserts # structural integrity (frontmatter, references present + cited) and — the # load-bearing check — the staleness verifier contract (SKILL-RESOURCE-PROTOCOL # §7, §10): the catalogued version-bearing facts (tokio, axum, serde) stay # named in the prose and the dated currency note stays present. Resolves paths # relative to itself so it works in the repo and once installed to # ~/.claude/skills/rust-ops/. # # Usage: bash tests/run.sh # Exit: 0 all pass, 1 one or more failures set -uo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SKILL="$(dirname "$HERE")" DOC="$SKILL/SKILL.md" REF="$SKILL/references" PASS=0; FAIL=0 ok() { PASS=$((PASS+1)); printf ' PASS %s\n' "$1"; } no() { FAIL=$((FAIL+1)); printf ' FAIL %s\n' "$1"; } has() { case "$2" in *"$1"*) ok "$3";; *) no "$3 (missing '$1')";; esac; } echo "=== rust-ops self-test ===" # ── SKILL.md frontmatter ─────────────────────────────────────────────────── echo "-- frontmatter --" [[ -f "$DOC" ]] && ok "SKILL.md present" || { no "SKILL.md missing"; echo "=== $PASS passed, $FAIL failed ==="; exit 1; } [[ "$(sed -n '1p' "$DOC")" == "---" ]] && ok "frontmatter fence opens at line 1" || no "no opening frontmatter fence" doc="$(cat "$DOC")" has 'name: rust-ops' "$doc" "frontmatter declares name: rust-ops" has 'description:' "$doc" "frontmatter has description" has 'license: MIT' "$doc" "frontmatter declares license" has 'author: claude-mods' "$doc" "frontmatter declares metadata.author" # ── references: the 6 documented files exist ─────────────────────────────── echo "-- references present --" EXPECT=(ownership-lifetimes traits-generics error-handling async-tokio ecosystem testing) for r in "${EXPECT[@]}"; do f="$REF/$r.md" [[ -f "$f" ]] && ok "$r.md present" || no "$r.md missing" done # ── every references/ citation in SKILL.md resolves (no ghost refs) ──────── # rust-ops cites its references as inline code spans (`./references/x.md`), # not markdown links — extract those spans and confirm each file exists. echo "-- cited references resolve --" linked=0; broken=0 while IFS= read -r rel; do [[ -z "$rel" ]] && continue linked=$((linked+1)) [[ -f "$SKILL/$rel" ]] || { no "SKILL.md cites missing file: $rel"; broken=$((broken+1)); } done < <(grep -oE '`(\./)?references/[^`#]+\.md`' "$DOC" | sed -E 's/^`//; s/`$//; s/^\.\///' | sort -u) [[ "$linked" -gt 0 ]] && ok "SKILL.md cites its references ($linked unique)" || no "SKILL.md cites no references" [[ "$broken" -eq 0 ]] && ok "all cited reference files resolve" || no "$broken reference citation(s) broken" # ── dated currency note present (verifier depends on it) ─────────────────── echo "-- currency note --" grep -qE 'as of 20[0-9]{2}' "$DOC" && ok "dated 'as of 20XX' currency note present" || no "no dated currency note" # ── staleness verifier: offline contract (SKILL-RESOURCE-PROTOCOL §7) ─────── echo "-- check-rust-facts.py (offline) --" VERIFIER="$SKILL/scripts/check-rust-facts.py" CATALOG="$SKILL/assets/rust-facts.json" ec() { local want="$1" lbl="$2"; shift 2; "$@" >/dev/null 2>&1; local got=$? [[ "$got" == "$want" ]] && ok "$lbl (exit $got)" || no "$lbl (want $want got $got)"; } # Pick a python that actually executes — skips the Windows Store python3 stub. PY="" for c in python python3 py; do if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PY="$c"; break; fi done [[ -f "$VERIFIER" ]] && ok "verifier present" || no "verifier missing" [[ -f "$CATALOG" ]] && ok "facts catalog present" || no "catalog missing" has "scripts/check-rust-facts.py" "$doc" "verifier cited from SKILL.md" if [[ -n "$PY" ]]; then TMP="$(mktemp -d)"; trap 'rm -rf "$TMP"' EXIT ec 0 "py_compile" "$PY" -m py_compile "$VERIFIER" ec 0 "--help" "$PY" "$VERIFIER" --help ec 0 "--offline consistent" "$PY" "$VERIFIER" --offline ec 2 "bad flag -> 2" "$PY" "$VERIFIER" --bogus ec 2 "conflicting modes -> 2" "$PY" "$VERIFIER" --offline --live jout="$("$PY" "$VERIFIER" --offline --json 2>/dev/null)" has 'claude-mods.rust-ops.facts/v1' "$jout" "--json envelope schema" ec 3 "missing catalog -> 3" "$PY" "$VERIFIER" --offline --catalog "$TMP/nope.json" printf '{"packages":"x"}' > "$TMP/bad.json" ec 4 "malformed catalog -> 4" "$PY" "$VERIFIER" --offline --catalog "$TMP/bad.json" printf '{"packages":[{"name":"zzznotreal","documented_major":1}]}' > "$TMP/drift.json" ec 10 "uncited package -> 10" "$PY" "$VERIFIER" --offline --catalog "$TMP/drift.json" else no "no working python to exercise the verifier" fi # ── summary ──────────────────────────────────────────────────────────────── echo "=== $PASS passed, $FAIL failed ===" [[ "$FAIL" -eq 0 ]] || exit 1
-
-
SKILL.md 11 KB
--- name: rust-ops description: "Rust development patterns, ownership, async, error handling, and ecosystem. Use for: rust, cargo, ownership, borrow checker, lifetime, tokio, serde, trait, Result, Option, async rust, crate, derive, impl, enum, pattern matching, Arc, Mutex, Send, Sync, thiserror, anyhow, clap, axum, sqlx, reqwest, rayon, tracing." license: MIT allowed-tools: "Read Write Bash" metadata: author: claude-mods related-skills: docker-ops, ci-cd-ops, testing-ops --- # Rust Operations Comprehensive Rust skill covering ownership, async, error handling, and the production ecosystem. > Ecosystem facts verified as of 2026-07. **Staleness check:** `python scripts/check-rust-facts.py --offline` asserts the catalogued version-bearing facts (tokio, axum, serde) are still named in the prose and the dated currency note above is present; run `--live` to confirm each crate's crates.io major still matches the documented major. Catalog: `assets/rust-facts.json`. ## Ownership Quick Reference ``` Who owns the value? │ ├─ Need to transfer ownership │ └─ Move: let s2 = s1; (s1 is invalid after this) │ ├─ Need to read without owning │ └─ Shared borrow: &T (multiple allowed, no mutation) │ ├─ Need to mutate without owning │ └─ Exclusive borrow: &mut T (only one, no other borrows) │ ├─ Need to share ownership across threads │ └─ Arc<T> (atomic reference counting) │ └─ Need mutation too? Arc<Mutex<T>> │ ├─ Need to share ownership single-threaded │ └─ Rc<T> (reference counting, not Send) │ └─ Need mutation too? Rc<RefCell<T>> │ └─ Need to avoid cloning large data └─ Cow<'a, T> (clone-on-write, borrows when possible) ``` ### The Borrow Rules 1. At any time, you can have **either** one `&mut T` **or** any number of `&T` 2. References must always be valid (no dangling) 3. These rules are enforced at compile time (zero runtime cost) ## Error Handling Decision Tree ``` What kind of error? │ ├─ Operation might not have a value (no error info needed) │ └─ Option<T>: Some(value) or None │ ├─ Library code (callers need to match on error variants) │ └─ thiserror: #[derive(Error)] enum with variants │ └─ Each variant can wrap source errors with #[from] │ ├─ Application code (just need context, not matching) │ └─ anyhow: anyhow::Result<T>, .context("msg") │ ├─ Converting between error types │ └─ impl From<SourceError> for MyError │ └─ Or use #[from] with thiserror │ └─ Truly unrecoverable (violating invariants) └─ panic!() or unwrap() - avoid in library code ``` ### thiserror (Library Errors) ```rust use thiserror::Error; #[derive(Debug, Error)] pub enum AppError { #[error("database error: {0}")] Database(#[from] sqlx::Error), #[error("not found: {entity} with id {id}")] NotFound { entity: &'static str, id: i64 }, #[error("validation failed: {0}")] Validation(String), } ``` ### anyhow (Application Errors) ```rust use anyhow::{Context, Result}; fn load_config(path: &str) -> Result<Config> { let content = std::fs::read_to_string(path) .context("failed to read config file")?; let config: Config = toml::from_str(&content) .context("failed to parse config")?; Ok(config) } ``` ### The ? Operator ```rust // ? on Result: returns Err early, unwraps Ok let file = File::open(path)?; // ? on Option: returns None early, unwraps Some let first = items.first()?; // Chain with map_err for context let port: u16 = env::var("PORT") .map_err(|_| AppError::Config("PORT not set"))? .parse() .map_err(|_| AppError::Config("PORT not a number"))?; ``` **Deep dive**: Load `./references/error-handling.md` for Result/Option combinators, error conversion patterns, panic/recover. ## Trait Design Quick Reference ### Common Derives ```rust #[derive(Debug, Clone, PartialEq, Eq, Hash)] // Value types #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] // API types #[derive(Debug, thiserror::Error)] // Error types ``` ### Trait Objects vs Generics | | Trait Objects (`dyn Trait`) | Generics (`T: Trait`) | |---|---|---| | Dispatch | Dynamic (vtable) | Static (monomorphized) | | Binary size | Smaller | Larger (per-type copies) | | Performance | Slight overhead | Zero-cost | | Heterogeneous collections | Yes | No | | Use when | Runtime polymorphism, plugin systems | Performance-critical, known types | ```rust // Generics (preferred when types known at compile time) fn process<T: Display>(item: T) { println!("{item}"); } // Trait objects (when you need heterogeneous collections) fn process_all(items: &[Box<dyn Display>]) { for item in items { println!("{item}"); } } ``` ### Key Traits to Know | Trait | Purpose | Auto-derive? | |-------|---------|-------------| | `Debug` | Debug formatting | Yes | | `Clone` | Explicit copy | Yes | | `Copy` | Implicit copy (small, stack-only) | Yes | | `Display` | User-facing formatting | No (impl manually) | | `From`/`Into` | Type conversion | No (impl `From`, get `Into` free) | | `Send` | Safe to send between threads | Auto | | `Sync` | Safe to share references between threads | Auto | | `Deref` | Smart pointer dereference | No | | `Iterator` | Iteration protocol | No | | `Default` | Default value | Yes | **Deep dive**: Load `./references/traits-generics.md` for associated types, supertraits, sealed traits, extension traits. ## Async Decision Tree ``` Do you need async? │ ├─ I/O-heavy (network, files, databases) │ └─ Yes. Use tokio. │ ├─ CPU-heavy computation │ └─ No. Use rayon for data parallelism. │ └─ Or tokio::task::spawn_blocking for mixing with async │ ├─ Simple scripts or CLI tools │ └─ Probably not. Blocking I/O is fine. │ └─ Yes, I need async: │ ├─ Runtime: tokio (dominant), or async-std ├─ HTTP client: reqwest ├─ HTTP server: axum (tower-based) or actix-web ├─ Database: sqlx (compile-time checked) └─ Structured logging: tracing ``` ### tokio Quick Start ```rust #[tokio::main] async fn main() -> anyhow::Result<()> { // Spawn concurrent tasks let (a, b) = tokio::join!( fetch_users(), fetch_orders(), ); // Select first to complete tokio::select! { result = long_operation() => handle(result), _ = tokio::time::sleep(Duration::from_secs(5)) => { eprintln!("timeout"); } } Ok(()) } ``` ### Channel Types | Channel | Use Case | Import | |---------|----------|--------| | `mpsc` | Multiple producers, single consumer | `tokio::sync::mpsc` | | `oneshot` | Single value, single use | `tokio::sync::oneshot` | | `broadcast` | Multiple consumers, all get every message | `tokio::sync::broadcast` | | `watch` | Single value, latest-only (config reload) | `tokio::sync::watch` | **Deep dive**: Load `./references/async-tokio.md` for spawn patterns, graceful shutdown, Mutex choice, async traits, streams. ## Cargo Quick Reference ```bash # Create project cargo new my-project # binary cargo new my-lib --lib # library # Build and run cargo build # debug cargo build --release # optimized cargo run -- args # build + run cargo run --example name # run example # Test cargo test # all tests cargo test test_name # specific test cargo test -- --nocapture # show println output # Dependencies cargo add serde --features derive # add dep cargo add tokio -F full # shorthand cargo update # update lock file # Check without building cargo check # fast type checking cargo clippy # lints cargo fmt # format # Workspace cargo test --workspace # test all crates cargo build -p my-crate # build specific crate ``` ### Feature Flags ```toml [features] default = ["json"] json = ["dep:serde_json"] full = ["json", "yaml", "toml"] [dependencies] serde_json = { version = "1", optional = true } ``` ### Release Profile Tuning ```toml [profile.release] lto = true # Link-time optimization: smaller, faster binaries codegen-units = 1 # Better optimization at the cost of compile time ``` ## Common Gotchas | Gotcha | Why | Fix | |--------|-----|-----| | `String` vs `&str` | Owned vs borrowed, function signatures | Accept `&str` in params, return `String` | | Borrow checker fight | Borrowing self while mutating | Split struct, use indices, clone (if cheap) | | Lifetime elision confusion | Hidden lifetimes in function signatures | Write them out explicitly to understand, then elide | | `impl Trait` in return | Different branches must return same type | Use `Box<dyn Trait>` for heterogeneous returns | | `tokio::Mutex` vs `std::Mutex` | `std::Mutex` can't be held across `.await` | Use `tokio::Mutex` across await points | | Orphan rule | Can't impl foreign trait for foreign type | Newtype pattern: `struct Wrapper(ForeignType)` | | `Pin` confusion | Required for self-referential async futures | Use `Box::pin()`, don't fight it | | `Send` bounds on async | Spawned futures must be `Send` | Avoid `Rc`, `RefCell` in async; use `Arc`, `Mutex` | | `.unwrap()` in production | Panics on None/Err | Use `?`, `.unwrap_or()`, `.expect("reason")` | ## serde Quick Reference ```rust use serde::{Serialize, Deserialize}; #[derive(Serialize, Deserialize)] #[serde(rename_all = "camelCase")] struct User { user_id: i64, display_name: String, #[serde(skip_serializing_if = "Option::is_none")] email: Option<String>, #[serde(default)] is_active: bool, #[serde(rename = "type")] user_type: UserType, #[serde(with = "chrono::serde::ts_seconds")] created_at: DateTime<Utc>, } // Serialize let json = serde_json::to_string(&user)?; let yaml = serde_yaml::to_string(&user)?; // Deserialize let user: User = serde_json::from_str(&json)?; ``` **Deep dive**: Load `./references/ecosystem.md` for serde advanced usage, clap, reqwest, sqlx, axum, tracing, rayon. ## Reference Files Load these for deep-dive topics. Each is self-contained. | Reference | When to Load | |-----------|-------------| | `./references/ownership-lifetimes.md` | Borrowing rules, lifetime annotations, elision, interior mutability, common borrow checker patterns | | `./references/traits-generics.md` | Trait design, associated types, supertraits, generics, constraints, sealed/extension traits | | `./references/error-handling.md` | Result/Option combinators, thiserror/anyhow deep dive, error conversion, panic/recover | | `./references/async-tokio.md` | tokio runtime, spawn, channels, select, streams, graceful shutdown, async traits, Mutex choice | | `./references/ecosystem.md` | serde advanced, clap, reqwest, sqlx, axum, tracing, rayon, itertools, Cow | | `./references/testing.md` | Unit/integration/doc tests, async tests, mockall, proptest, criterion benchmarks | ## See Also - `docker-ops` - Multi-stage builds for Rust (scratch/distroless, cargo-chef for layer caching) - `ci-cd-ops` - Rust CI pipelines, cargo caching, cross-compilation - `testing-ops` - Cross-language testing strategies
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.