Agent Skills: Rust Fundamentals

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.

UncategorizedID: arustydev/ai/lang-rust-dev

Repository

aRustyDevLicense: AGPL-3.0
72

Install this agent skill to your local

pnpm dlx add-skill https://github.com/aRustyDev/agents/tree/HEAD/content/skills/lang-rust-dev

Skill Files

Browse the full folder contents for lang-rust-dev.

Download Skill

Loading file tree…

content/skills/lang-rust-dev/SKILL.md

Skill Metadata

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, channels
  • patterns-serialization-dev - JSON, validation, struct tags
  • patterns-metaprogramming-dev - Decorators, macros, annotations

References