name: lang-rust-dev description: Foundational Rust patterns covering core syntax, traits, generics, lifetimes, and common idioms. Use when writing Rust code, understanding ownership basics, working with Option/Result, or needing guidance on which specialized Rust skill to use. This is the entry point for Rust development.
Rust Fundamentals
Foundational Rust patterns and core language features. This skill serves as both a reference for common patterns and an index to specialized Rust skills.
Overview
┌─────────────────────────────────────────────────────────────────┐
│ Rust Skill Hierarchy │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────┐ │
│ │ lang-rust-dev │ ◄── You are here │
│ │ (foundation) │ │
│ └─────────┬─────────┘ │
│ │ │
│ ┌────────────┬───────────┼───────────┬────────────┐ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ ▼ │
│ ┌────────┐ ┌──────────┐ ┌────────┐ ┌─────────┐ ┌──────────┐ │
│ │ errors │ │ cargo │ │library │ │ memory │ │ profiling│ │
│ │ -dev │ │ -dev │ │ -dev │ │ -eng │ │ -eng │ │
│ └────────┘ └──────────┘ └────────┘ └─────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
This skill covers:
- Core syntax (structs, enums, match, impl blocks)
- Traits and generics basics
- Lifetime fundamentals
- Option and Result patterns
- Iterators and closures
- Common idioms and conventions
This skill does NOT cover (see specialized skills):
- Error handling with error-stack →
lang-rust-errors-dev - Cargo.toml and dependencies →
lang-rust-cargo-dev - Library/crate publishing →
lang-rust-library-dev - Documentation patterns →
lang-rust-docs-dev - Memory safety engineering →
lang-rust-memory-eng - Benchmarking →
lang-rust-benchmarking-eng - Profiling/debugging →
lang-rust-profiling-eng
Quick Reference
| Task | Pattern |
|---|---|
| Create struct | struct Name { field: Type } |
| Create enum | enum Name { Variant1, Variant2(T) } |
| Implement trait | impl Trait for Type { ... } |
| Generic function | fn name<T: Trait>(x: T) -> T |
| Lifetime annotation | fn name<'a>(x: &'a str) -> &'a str |
| Error propagation | let x = fallible()?; |
| Pattern match | match value { Pattern => expr } |
| Iterate | for item in collection { ... } |
| Map/filter | iter.map(|x| ...).filter(|x| ...) |
Skill Routing
Use this table to find the right specialized skill:
| When you need to... | Use this skill |
|---|---|
| Handle errors with Result/Report types | lang-rust-errors-dev |
| Configure Cargo.toml, add dependencies | lang-rust-cargo-dev |
| Design public APIs, publish crates | lang-rust-library-dev |
| Write documentation, rustdoc | lang-rust-docs-dev |
| Understand ownership deeply, unsafe code | lang-rust-memory-eng |
| Write benchmarks, measure performance | lang-rust-benchmarking-eng |
| Profile code, find bottlenecks | lang-rust-profiling-eng |
Core Types
Structs
// Named fields
struct User {
name: String,
email: String,
age: u32,
}
// Tuple struct
struct Point(f64, f64);
// Unit struct
struct Marker;
// Creating instances
let user = User {
name: String::from("Alice"),
email: String::from("alice@example.com"),
age: 30,
};
// Struct update syntax
let user2 = User {
email: String::from("bob@example.com"),
..user // Take remaining fields from user
};
// Destructuring
let User { name, email, .. } = user2;
Enums
// Simple enum
enum Direction {
North,
South,
East,
West,
}
// Enum with data
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(u8, u8, u8),
}
// Using enums
let msg = Message::Move { x: 10, y: 20 };
match msg {
Message::Quit => println!("Quit"),
Message::Move { x, y } => println!("Move to {x}, {y}"),
Message::Write(text) => println!("Write: {text}"),
Message::ChangeColor(r, g, b) => println!("Color: {r},{g},{b}"),
}
Option and Result
// Option: value that might not exist
fn find_user(id: u32) -> Option<User> {
if id == 1 {
Some(User { /* ... */ })
} else {
None
}
}
// Using Option
match find_user(1) {
Some(user) => println!("Found: {}", user.name),
None => println!("Not found"),
}
// Option methods
let name = find_user(1)
.map(|u| u.name)
.unwrap_or_else(|| String::from("Anonymous"));
// Result: operation that might fail
fn parse_config(path: &str) -> Result<Config, ConfigError> {
let content = std::fs::read_to_string(path)?;
let config = serde_json::from_str(&content)?;
Ok(config)
}
// Error propagation with ?
fn process() -> Result<(), Error> {
let config = parse_config("config.json")?; // Returns early on error
// ... use config
Ok(())
}
Pattern Matching
Match Expressions
let x = 5;
match x {
1 => println!("one"),
2 | 3 => println!("two or three"),
4..=6 => println!("four through six"),
n if n > 10 => println!("greater than ten: {n}"),
_ => println!("something else"),
}
// Destructuring in match
let point = (3, 4);
match point {
(0, 0) => println!("origin"),
(x, 0) => println!("on x-axis at {x}"),
(0, y) => println!("on y-axis at {y}"),
(x, y) => println!("at ({x}, {y})"),
}
If Let and Let Else
// if let: match single pattern
if let Some(user) = find_user(1) {
println!("Found: {}", user.name);
}
// let else: match or diverge
fn get_name(id: u32) -> String {
let Some(user) = find_user(id) else {
return String::from("Unknown");
};
user.name
}
Traits
Defining Traits
trait Summary {
// Required method
fn summarize(&self) -> String;
// Default implementation
fn preview(&self) -> String {
format!("{}...", &self.summarize()[..50])
}
}
Implementing Traits
struct Article {
title: String,
content: String,
}
impl Summary for Article {
fn summarize(&self) -> String {
format!("{}: {}", self.title, self.content)
}
}
// Use the trait
let article = Article { /* ... */ };
println!("{}", article.summarize());
Common Standard Traits
| Trait | Purpose | Derive? |
|---|---|---|
Debug | Debug formatting {:?} | Yes |
Clone | Explicit duplication | Yes |
Copy | Implicit copying | Yes (if all fields Copy) |
Default | Default value | Yes |
PartialEq / Eq | Equality comparison | Yes |
PartialOrd / Ord | Ordering | Yes |
Hash | Hash for HashMap keys | Yes |
Display | User-facing formatting | No |
From / Into | Type conversion | No |
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct Config {
name: String,
value: i32,
}
Trait Bounds
// Function with trait bound
fn print_summary<T: Summary>(item: &T) {
println!("{}", item.summarize());
}
// Multiple bounds
fn process<T: Summary + Clone>(item: T) { /* ... */ }
// Where clause (cleaner for complex bounds)
fn complex<T, U>(t: T, u: U) -> String
where
T: Summary + Clone,
U: Debug + Default,
{
// ...
}
Generics
Generic Functions
fn largest<T: PartialOrd>(list: &[T]) -> &T {
let mut largest = &list[0];
for item in list {
if item > largest {
largest = item;
}
}
largest
}
Generic Structs
struct Wrapper<T> {
value: T,
}
impl<T> Wrapper<T> {
fn new(value: T) -> Self {
Wrapper { value }
}
fn get(&self) -> &T {
&self.value
}
}
// Conditional implementation
impl<T: Display> Wrapper<T> {
fn print(&self) {
println!("{}", self.value);
}
}
Generic Enums
// Option and Result are generic enums
enum Option<T> {
Some(T),
None,
}
enum Result<T, E> {
Ok(T),
Err(E),
}
Lifetimes
Basic Lifetime Annotations
// Lifetime ensures returned reference is valid
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
// Usage
let s1 = String::from("short");
let s2 = String::from("longer string");
let result = longest(&s1, &s2);
Lifetime in Structs
// Struct containing references
struct Excerpt<'a> {
text: &'a str,
}
impl<'a> Excerpt<'a> {
fn level(&self) -> i32 {
3 // Doesn't use the reference, no annotation needed
}
fn announce(&self, announcement: &str) -> &'a str {
println!("Attention: {announcement}");
self.text
}
}
Lifetime Elision
The compiler infers lifetimes in common cases:
// These are equivalent:
fn first_word(s: &str) -> &str { /* ... */ }
fn first_word<'a>(s: &'a str) -> &'a str { /* ... */ }
// Rules:
// 1. Each input reference gets its own lifetime
// 2. If exactly one input lifetime, output gets same
// 3. If &self, output gets lifetime of self
Iterators
Creating Iterators
let v = vec![1, 2, 3, 4, 5];
// Borrowing iterator
for x in &v {
println!("{x}");
}
// Consuming iterator
for x in v {
println!("{x}");
}
// Mutable iterator
let mut v = vec![1, 2, 3];
for x in &mut v {
*x *= 2;
}
Iterator Adapters
let v = vec![1, 2, 3, 4, 5];
// map: transform each element
let doubled: Vec<_> = v.iter().map(|x| x * 2).collect();
// filter: keep matching elements
let evens: Vec<_> = v.iter().filter(|x| *x % 2 == 0).collect();
// chain adapters
let result: Vec<_> = v.iter()
.filter(|x| *x > 2)
.map(|x| x * 10)
.collect();
// find: first matching element
let found = v.iter().find(|x| **x > 3);
// fold: accumulate
let sum: i32 = v.iter().fold(0, |acc, x| acc + x);
// Or use sum()
let sum: i32 = v.iter().sum();
Common Iterator Methods
| Method | Purpose |
|---|---|
map | Transform elements |
filter | Keep matching elements |
filter_map | Filter and transform in one |
flat_map | Map and flatten |
take(n) | First n elements |
skip(n) | Skip first n elements |
enumerate | Add index to elements |
zip | Combine two iterators |
collect | Collect into container |
fold | Reduce to single value |
find | First matching element |
any / all | Boolean predicates |
Closures
Closure Syntax
// Full syntax
let add = |a: i32, b: i32| -> i32 { a + b };
// Type inference
let add = |a, b| a + b;
// Single expression (no braces needed)
let double = |x| x * 2;
// Capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier;
Closure Traits
| Trait | Captures | Can be called |
|---|---|---|
Fn | Immutable borrow | Multiple times |
FnMut | Mutable borrow | Multiple times |
FnOnce | Takes ownership | Once |
// Function taking a closure
fn apply<F>(f: F, x: i32) -> i32
where
F: Fn(i32) -> i32,
{
f(x)
}
let result = apply(|x| x * 2, 5);
Move Closures
// Force closure to take ownership
let s = String::from("hello");
let print = move || println!("{s}");
// s is no longer valid here
print(); // Works because closure owns s
Common Idioms
Builder Pattern
struct RequestBuilder {
url: String,
method: String,
headers: Vec<(String, String)>,
}
impl RequestBuilder {
fn new(url: impl Into<String>) -> Self {
Self {
url: url.into(),
method: String::from("GET"),
headers: Vec::new(),
}
}
fn method(mut self, method: impl Into<String>) -> Self {
self.method = method.into();
self
}
fn header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.headers.push((key.into(), value.into()));
self
}
fn build(self) -> Request {
Request { /* ... */ }
}
}
// Usage
let request = RequestBuilder::new("https://api.example.com")
.method("POST")
.header("Content-Type", "application/json")
.build();
Newtype Pattern
// Wrap primitive types for type safety
struct UserId(u64);
struct OrderId(u64);
fn process_user(id: UserId) { /* ... */ }
fn process_order(id: OrderId) { /* ... */ }
// Can't mix them up
let user_id = UserId(1);
let order_id = OrderId(1);
// process_user(order_id); // Compile error!
Type State Pattern
// Compile-time state machine
struct Request<State> {
url: String,
_state: std::marker::PhantomData<State>,
}
struct Unvalidated;
struct Validated;
impl Request<Unvalidated> {
fn validate(self) -> Result<Request<Validated>, Error> {
// Validation logic
Ok(Request {
url: self.url,
_state: std::marker::PhantomData,
})
}
}
impl Request<Validated> {
fn send(self) -> Response {
// Only valid requests can be sent
}
}
Troubleshooting
Ownership Errors
Problem: value borrowed here after move
let s = String::from("hello");
let s2 = s;
println!("{s}"); // Error: s was moved to s2
Fix: Clone if you need both, or use references:
let s = String::from("hello");
let s2 = s.clone();
println!("{s}"); // Works
Lifetime Errors
Problem: missing lifetime specifier
fn get_first(s: &str, t: &str) -> &str {
s // Error: which lifetime?
}
Fix: Add explicit lifetimes:
fn get_first<'a>(s: &'a str, _t: &str) -> &'a str {
s
}
Trait Bound Errors
Problem: the trait X is not implemented for Y
fn print_it<T>(x: T) {
println!("{}", x); // Error: T doesn't implement Display
}
Fix: Add trait bound:
fn print_it<T: std::fmt::Display>(x: T) {
println!("{}", x);
}
Mutability Errors
Problem: cannot borrow as mutable
let v = vec![1, 2, 3];
v.push(4); // Error: v is not mutable
Fix: Make it mutable:
let mut v = vec![1, 2, 3];
v.push(4);
Module System
Rust uses a module system to organize code into logical units with explicit visibility control.
Module Basics
// src/lib.rs or src/main.rs
// Inline module
mod math {
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn private_helper() -> i32 {
42
}
}
// Use items from module
use math::add;
fn main() {
let sum = add(2, 3);
// math::private_helper(); // Error: function is private
}
File-Based Modules
src/
├── main.rs # Crate root
├── lib.rs # Library crate root (if both bin and lib)
├── config.rs # mod config;
└── network/
├── mod.rs # mod network;
├── client.rs # mod client; (in mod.rs)
└── server.rs # mod server; (in mod.rs)
// src/main.rs
mod config; // Loads from src/config.rs
mod network; // Loads from src/network/mod.rs
use config::Settings;
use network::client::Client;
// src/network/mod.rs
pub mod client; // Loads from src/network/client.rs
pub mod server;
Visibility Rules
mod outer {
pub mod inner {
pub fn public_fn() {} // Visible everywhere
pub(crate) fn crate_fn() {} // Visible in crate
pub(super) fn parent_fn() {} // Visible in parent module
pub(in crate::outer) fn outer_fn() {} // Visible in specific path
fn private_fn() {} // Only this module
}
}
Re-exports
// src/lib.rs - Flatten the public API
mod internal {
pub mod config {
pub struct Settings { /* ... */ }
}
pub mod network {
pub struct Client { /* ... */ }
}
}
// Re-export for cleaner external API
pub use internal::config::Settings;
pub use internal::network::Client;
// External users can now:
// use mycrate::Settings;
// Instead of:
// use mycrate::internal::config::Settings;
Prelude Pattern
// src/prelude.rs
pub use crate::config::Settings;
pub use crate::error::{Error, Result};
pub use crate::traits::{Serialize, Deserialize};
// Users can import everything commonly needed:
// use mycrate::prelude::*;
Path Types
| Path | Meaning |
|---|---|
crate:: | Start from crate root |
self:: | Current module |
super:: | Parent module |
::path | External crate (Rust 2018+: crate name) |
Concurrency
Rust provides fearless concurrency through its ownership system. The type system prevents data races at compile time.
Threads
use std::thread;
// Spawn a thread
let handle = thread::spawn(|| {
println!("Hello from thread!");
});
handle.join().unwrap(); // Wait for thread to finish
// Move data into thread
let data = vec![1, 2, 3];
let handle = thread::spawn(move || {
println!("Data: {:?}", data);
});
Message Passing (Channels)
use std::sync::mpsc; // Multiple producer, single consumer
let (tx, rx) = mpsc::channel();
// Clone sender for multiple producers
let tx2 = tx.clone();
thread::spawn(move || {
tx.send("Hello").unwrap();
});
thread::spawn(move || {
tx2.send("World").unwrap();
});
// Receive messages
for received in rx {
println!("Got: {received}");
}
Shared State (Mutex, Arc)
use std::sync::{Arc, Mutex};
// Arc: Atomic Reference Counted (thread-safe Rc)
// Mutex: Mutual exclusion for safe mutable access
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = thread::spawn(move || {
let mut num = counter.lock().unwrap();
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!("Result: {}", *counter.lock().unwrap());
Async/Await
// Requires async runtime (tokio, async-std)
// Cargo.toml: tokio = { version = "1", features = ["full"] }
use tokio;
async fn fetch_data(url: &str) -> Result<String, reqwest::Error> {
reqwest::get(url).await?.text().await
}
async fn process() {
let data = fetch_data("https://api.example.com").await.unwrap();
println!("{data}");
}
// Run async main
#[tokio::main]
async fn main() {
process().await;
}
// Concurrent execution
async fn fetch_all() {
let (a, b) = tokio::join!(
fetch_data("https://api.example.com/a"),
fetch_data("https://api.example.com/b"),
);
}
Send and Sync Traits
| Trait | Meaning |
|---|---|
Send | Safe to send between threads |
Sync | Safe to share references between threads |
// Most types are Send + Sync automatically
// Rc is not Send (use Arc instead)
// Cell/RefCell are not Sync (use Mutex instead)
See also: patterns-concurrency-dev for cross-language concurrency patterns
Serialization
Rust uses the serde framework for serialization and deserialization.
Basic Serde Usage
// Cargo.toml:
// serde = { version = "1.0", features = ["derive"] }
// serde_json = "1.0"
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct User {
name: String,
email: String,
age: u32,
}
fn main() -> Result<(), serde_json::Error> {
let user = User {
name: String::from("Alice"),
email: String::from("alice@example.com"),
age: 30,
};
// Serialize to JSON
let json = serde_json::to_string(&user)?;
println!("{json}");
// Deserialize from JSON
let parsed: User = serde_json::from_str(&json)?;
println!("{:?}", parsed);
Ok(())
}
Serde Attributes
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] // All fields as camelCase
struct Config {
#[serde(rename = "api_key")] // Custom field name
key: String,
#[serde(default)] // Use Default if missing
retries: u32,
#[serde(skip_serializing_if = "Option::is_none")]
email: Option<String>,
#[serde(skip)] // Never serialize/deserialize
internal_state: u32,
#[serde(flatten)] // Flatten nested struct
metadata: Metadata,
}
#[derive(Serialize, Deserialize)]
struct Metadata {
version: String,
author: String,
}
Enum Serialization
#[derive(Serialize, Deserialize)]
#[serde(tag = "type")] // Internally tagged
enum Message {
#[serde(rename = "text")]
Text { content: String },
#[serde(rename = "image")]
Image { url: String, width: u32 },
}
// Serializes as: {"type": "text", "content": "Hello"}
Custom Serialization
use serde::{Serializer, Deserializer};
#[derive(Serialize, Deserialize)]
struct Data {
#[serde(serialize_with = "serialize_as_string")]
#[serde(deserialize_with = "deserialize_from_string")]
value: u64,
}
fn serialize_as_string<S>(value: &u64, s: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
s.serialize_str(&value.to_string())
}
fn deserialize_from_string<'de, D>(d: D) -> Result<u64, D::Error>
where
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(d)?;
s.parse().map_err(serde::de::Error::custom)
}
Other Formats
// YAML: serde_yaml = "0.9"
let yaml = serde_yaml::to_string(&data)?;
let parsed: Data = serde_yaml::from_str(&yaml)?;
// TOML: toml = "0.8"
let toml = toml::to_string(&data)?;
let parsed: Data = toml::from_str(&toml)?;
// MessagePack: rmp-serde = "1.1"
let msgpack = rmp_serde::to_vec(&data)?;
let parsed: Data = rmp_serde::from_slice(&msgpack)?;
See also: patterns-serialization-dev for cross-language serialization patterns
Build and Dependencies
Rust uses Cargo as its build system and package manager.
Cargo.toml Basics
[package]
name = "myproject"
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
description = "A brief description"
license = "MIT OR Apache-2.0"
repository = "https://github.com/user/project"
[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
reqwest = "0.11"
[dev-dependencies]
criterion = "0.5"
[build-dependencies]
cc = "1.0"
[features]
default = ["json"]
json = ["serde_json"]
full = ["json", "yaml"]
[[bin]]
name = "myapp"
path = "src/main.rs"
[[bench]]
name = "my_benchmark"
harness = false
Dependency Specification
# Version requirements
exact = "=1.0.0" # Exactly 1.0.0
caret = "^1.2.3" # >=1.2.3, <2.0.0 (default)
tilde = "~1.2.3" # >=1.2.3, <1.3.0
wildcard = "1.*" # >=1.0.0, <2.0.0
# Features
serde = { version = "1.0", features = ["derive"], default-features = false }
# Git dependencies
mylib = { git = "https://github.com/user/mylib", branch = "main" }
mylib = { git = "https://github.com/user/mylib", tag = "v1.0.0" }
mylib = { git = "https://github.com/user/mylib", rev = "abc123" }
# Path dependencies (local development)
mylib = { path = "../mylib" }
# Optional dependencies
serde_json = { version = "1.0", optional = true }
Common Cargo Commands
| Command | Purpose |
|---|---|
cargo build | Compile the project |
cargo build --release | Compile with optimizations |
cargo run | Build and run |
cargo test | Run tests |
cargo check | Fast type checking (no codegen) |
cargo clippy | Lint code |
cargo fmt | Format code |
cargo doc --open | Generate and open docs |
cargo update | Update dependencies |
cargo add <crate> | Add a dependency |
cargo tree | Show dependency tree |
Workspace Configuration
# Root Cargo.toml
[workspace]
members = [
"crates/core",
"crates/cli",
"crates/web",
]
resolver = "2"
[workspace.package]
version = "0.1.0"
edition = "2021"
license = "MIT"
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
# crates/core/Cargo.toml
[package]
name = "myproject-core"
version.workspace = true
edition.workspace = true
[dependencies]
serde.workspace = true
Build Scripts
// build.rs - Runs before compilation
fn main() {
// Tell Cargo to rerun if file changes
println!("cargo:rerun-if-changed=src/proto/schema.proto");
// Set environment variable for compilation
println!("cargo:rustc-env=BUILD_VERSION=1.0.0");
// Add link search path
println!("cargo:rustc-link-search=/usr/local/lib");
}
See also: lang-rust-cargo-dev for advanced Cargo configuration
Testing
Rust has built-in testing support with cargo test.
Unit Tests
// src/lib.rs
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
fn private_helper() -> i32 {
42
}
#[cfg(test)]
mod tests {
use super::*; // Import from parent module
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
#[test]
fn test_add_negative() {
assert_eq!(add(-1, 1), 0);
}
#[test]
fn test_private_helper() {
// Can test private functions
assert_eq!(private_helper(), 42);
}
#[test]
#[should_panic(expected = "divide by zero")]
fn test_panic() {
divide(1, 0);
}
#[test]
fn test_result() -> Result<(), String> {
let result = parse_number("42")?;
assert_eq!(result, 42);
Ok(())
}
}
Integration Tests
tests/
├── integration_test.rs # Each file is a separate test crate
└── common/
└── mod.rs # Shared test utilities
// tests/integration_test.rs
use myproject::add;
mod common; // Load shared utilities
#[test]
fn test_add_integration() {
common::setup();
assert_eq!(add(2, 3), 5);
}
Assertions
#[test]
fn test_assertions() {
// Equality
assert_eq!(actual, expected);
assert_ne!(actual, not_expected);
// Boolean
assert!(condition);
assert!(!condition);
// With custom message
assert_eq!(result, 42, "Expected 42, got {}", result);
// Debug assertions (only in debug builds)
debug_assert!(condition);
}
Test Attributes
#[test]
fn normal_test() {}
#[test]
#[ignore] // Skip by default, run with --ignored
fn slow_test() {}
#[test]
#[should_panic]
fn test_panics() {
panic!("This should panic");
}
#[test]
#[should_panic(expected = "specific message")]
fn test_specific_panic() {
panic!("specific message here");
}
Running Tests
cargo test # Run all tests
cargo test test_name # Run tests matching name
cargo test -- --nocapture # Show println! output
cargo test -- --test-threads=1 # Run sequentially
cargo test --ignored # Run ignored tests
cargo test --test integration # Run specific test file
Test Organization
// Group related tests
mod parsing_tests {
use super::*;
#[test]
fn test_parse_number() { /* ... */ }
#[test]
fn test_parse_string() { /* ... */ }
}
// Setup and teardown
struct TestFixture {
temp_dir: tempfile::TempDir,
}
impl TestFixture {
fn new() -> Self {
Self {
temp_dir: tempfile::tempdir().unwrap(),
}
}
}
impl Drop for TestFixture {
fn drop(&mut self) {
// Cleanup happens automatically
}
}
#[test]
fn test_with_fixture() {
let fixture = TestFixture::new();
// Use fixture.temp_dir
}
Mocking (with mockall)
// Cargo.toml: mockall = "0.11"
use mockall::{automock, predicate::*};
#[automock]
trait Database {
fn get_user(&self, id: u32) -> Option<User>;
}
#[test]
fn test_with_mock() {
let mut mock = MockDatabase::new();
mock.expect_get_user()
.with(eq(1))
.returning(|_| Some(User { name: "Alice".into() }));
let result = process_user(&mock, 1);
assert!(result.is_ok());
}
Property-Based Testing (with proptest)
// Cargo.toml: proptest = "1.0"
use proptest::prelude::*;
proptest! {
#[test]
fn test_add_commutative(a: i32, b: i32) {
prop_assert_eq!(add(a, b), add(b, a));
}
#[test]
fn test_parse_roundtrip(s in "[a-z]{1,10}") {
let parsed = parse(&s);
prop_assert!(parsed.is_ok());
}
}
Metaprogramming
Rust provides powerful metaprogramming through macros. There are two main types: declarative macros (macro_rules!) and procedural macros (derive, attribute, and function-like).
Declarative Macros (macro_rules!)
// Simple macro
macro_rules! say_hello {
() => {
println!("Hello!");
};
}
say_hello!(); // Prints: Hello!
// Macro with arguments
macro_rules! create_function {
($name:ident) => {
fn $name() {
println!("Called {:?}", stringify!($name));
}
};
}
create_function!(foo);
foo(); // Prints: Called "foo"
// Macro with expression repetition
macro_rules! vec_of_strings {
($($x:expr),* $(,)?) => {
vec![$($x.to_string()),*]
};
}
let v = vec_of_strings!["a", "b", "c"];
Fragment Specifiers
| Specifier | Matches | Example |
|---|---|---|
$x:ident | Identifier | foo, MyStruct |
$x:expr | Expression | 1 + 2, foo() |
$x:ty | Type | i32, Vec<String> |
$x:pat | Pattern | Some(x), _ |
$x:stmt | Statement | let x = 1; |
$x:block | Block | { ... } |
$x:item | Item | fn foo() {} |
$x:path | Path | std::io::Error |
$x:tt | Token tree | Any single token |
$x:literal | Literal | "hello", 42 |
Macro Patterns
// Multiple match arms
macro_rules! calculate {
// Single value
($e:expr) => { $e };
// Two values with operator
($left:expr, $op:tt, $right:expr) => {
$left $op $right
};
}
let a = calculate!(5); // 5
let b = calculate!(5, +, 3); // 8
// Recursive macro for variadic arguments
macro_rules! sum {
($x:expr) => { $x };
($x:expr, $($rest:expr),+) => {
$x + sum!($($rest),+)
};
}
let total = sum!(1, 2, 3, 4); // 10
Derive Macros
Derive macros generate trait implementations automatically.
// Using built-in derives
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
struct User {
name: String,
age: u32,
}
// Using third-party derives
use serde::{Serialize, Deserialize};
#[derive(Serialize, Deserialize)]
struct Config {
host: String,
port: u16,
}
Creating Custom Derive Macros
// In a proc-macro crate (Cargo.toml: proc-macro = true)
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};
#[proc_macro_derive(MyTrait)]
pub fn derive_my_trait(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = input.ident;
let expanded = quote! {
impl MyTrait for #name {
fn describe(&self) -> String {
format!("This is a {}", stringify!(#name))
}
}
};
TokenStream::from(expanded)
}
// Usage
#[derive(MyTrait)]
struct MyStruct;
Derive Macro with Attributes
// Macro definition
#[proc_macro_derive(Builder, attributes(builder))]
pub fn derive_builder(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
// Parse #[builder(...)] attributes
// Generate builder pattern implementation
// ...
}
// Usage
#[derive(Builder)]
struct Command {
#[builder(default = "false")]
verbose: bool,
#[builder(each = "arg")]
args: Vec<String>,
}
Attribute Macros
// Macro definition (in proc-macro crate)
#[proc_macro_attribute]
pub fn route(attr: TokenStream, item: TokenStream) -> TokenStream {
let attr = parse_macro_input!(attr as LitStr);
let item = parse_macro_input!(item as ItemFn);
let fn_name = &item.sig.ident;
let expanded = quote! {
#item
inventory::submit! {
Route {
path: #attr,
handler: #fn_name,
}
}
};
TokenStream::from(expanded)
}
// Usage
#[route("/api/users")]
fn get_users() -> Response {
// ...
}
Function-like Procedural Macros
// Macro definition
#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as LitStr);
let query = input.value();
// Validate SQL at compile time
// Generate typed query code
let expanded = quote! {
Query::new(#query)
};
TokenStream::from(expanded)
}
// Usage
let query = sql!("SELECT * FROM users WHERE id = $1");
Common Proc-Macro Crates
| Crate | Purpose | Example |
|---|---|---|
syn | Parse Rust code | parse_macro_input! |
quote | Generate Rust code | quote! { ... } |
proc-macro2 | TokenStream utilities | Span manipulation |
darling | Derive macro helpers | Attribute parsing |
Macro Hygiene
macro_rules! using_x {
($e:expr) => {
{
let x = 42; // This x is hygienic
$e // $e refers to caller's x
}
};
}
let x = 10;
let result = using_x!(x + 1); // Uses caller's x=10, not macro's x=42
assert_eq!(result, 11);
Debug Macros
// Print macro expansion
// Run: cargo expand
// Or: cargo expand --lib path::to::module
// Compile-time debugging
macro_rules! debug_macro {
($($arg:tt)*) => {
compile_error!(concat!("Debug: ", stringify!($($arg)*)));
};
}
See Also
patterns-metaprogramming-dev- Cross-language macro/decorator patterns
Cross-Cutting Patterns
For cross-language comparison and translation patterns, see:
patterns-concurrency-dev- Async/await, threads, channelspatterns-serialization-dev- JSON, validation, struct tagspatterns-metaprogramming-dev- Decorators, macros, annotations
References
- The Rust Book
- Rust by Example
- Rust Reference
- Specialized skills:
lang-rust-errors-dev,lang-rust-cargo-dev,lang-rust-library-dev,lang-rust-docs-dev,lang-rust-memory-eng