Back to skills
extension
Category: Development & EngineeringNo API key required

rust-dev-guidelines

Idiomatic Rust development patterns for async applications. Covers error handling with Result/Option, ownership and borrowing, async/await with Tokio, traits and generics, serde serialization, Arc/Mutex for shared state, and clippy best practices. Use when writing Rust code, refactoring, handling errors, or implementing async patterns.

personAuthor: jakexiaohubgithub

Rust Development Guidelines

Purpose

Comprehensive guide for writing idiomatic Rust code in this project. Focuses on patterns used in async Discord bots with external API integrations.

When to Use

  • Writing new Rust code
  • Refactoring existing code
  • Implementing error handling
  • Working with async/await
  • Using shared state (Arc, Mutex, RwLock)
  • Serialization with serde

Error Handling

Use Result<T, E> for Fallible Operations

// GOOD: Return Result for operations that can fail
pub async fn load_killmail(&self, url: String) -> Result<Killmail, String> {
    let response = self.client
        .get(&url)
        .send()
        .await
        .map_err(|e| format!("HTTP error: {}", e))?;

    response.json::<Killmail>()
        .await
        .map_err(|e| format!("Parse error: {}", e))
}

// BAD: Using unwrap/expect in production code paths
let data = response.json().await.unwrap(); // Panics on error!

Use ? Operator for Propagation

// GOOD: Clean error propagation
async fn process(&self) -> Result<Data, Error> {
    let response = self.fetch().await?;
    let parsed = self.parse(response)?;
    Ok(parsed)
}

// AVOID: Verbose match chains
async fn process(&self) -> Result<Data, Error> {
    let response = match self.fetch().await {
        Ok(r) => r,
        Err(e) => return Err(e),
    };
    // ...
}

Use Option<T> for Optional Values

// GOOD: Use Option for values that may not exist
pub fn get_system_name(&self, id: u64) -> Option<&String> {
    self.systems.get(&id)
}

// Usage with combinators
let name = app_state.get_system_name(system_id)
    .unwrap_or(&"Unknown".to_string());

// Or with if-let
if let Some(name) = app_state.get_system_name(system_id) {
    info!("System: {}", name);
}

See resources/error-handling.md for custom error types and thiserror patterns.


Async Patterns (Tokio)

Async Functions

// Mark async functions with async keyword
pub async fn fetch_data(&self) -> Result<Data, Error> {
    let response = self.client.get(url).send().await?;
    Ok(response.json().await?)
}

Spawning Tasks

// Fire-and-forget background task
tokio::spawn(async move {
    if let Err(e) = process_item(item).await {
        error!("Background task failed: {}", e);
    }
});

// Task with join handle
let handle = tokio::spawn(async move {
    expensive_computation().await
});
let result = handle.await?;

Concurrent Operations

// Run multiple futures concurrently
let (result1, result2) = tokio::join!(
    fetch_user(user_id),
    fetch_permissions(user_id)
);

// Select first to complete
tokio::select! {
    result = async_operation() => handle(result),
    _ = tokio::time::sleep(Duration::from_secs(5)) => timeout(),
}

See resources/async-patterns.md for channels, timeouts, and cancellation.


Ownership and Borrowing

Prefer References Over Clones

// GOOD: Borrow when you don't need ownership
fn process_data(data: &ZkData) {
    // Read-only access
}

// AVOID: Unnecessary clone
fn process_data(data: ZkData) {
    // Takes ownership when not needed
}

Use Arc for Shared Ownership

// Shared state across async tasks
let app_state = Arc::new(AppState::new(...));

// Clone Arc (cheap, just increments refcount)
let state_clone = app_state.clone();
tokio::spawn(async move {
    state_clone.do_something().await;
});

Use RwLock for Interior Mutability

// Multiple readers OR single writer
pub struct AppState {
    pub subscriptions: RwLock<HashMap<GuildId, Vec<Subscription>>>,
}

// Reading (many concurrent readers allowed)
let subs = app_state.subscriptions.read().unwrap();
let guild_subs = subs.get(&guild_id);

// Writing (exclusive access)
let mut subs = app_state.subscriptions.write().unwrap();
subs.insert(guild_id, new_subscriptions);

Structs and Serialization

Derive Common Traits

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Killmail {
    pub killmail_id: u64,
    pub killmail_time: String,
    pub solar_system_id: u64,
    pub victim: Victim,
    pub attackers: Vec<Attacker>,
}

Use #[serde(...)] for JSON Customization

#[derive(Deserialize)]
pub struct EsiResponse {
    #[serde(rename = "killmail_id")]
    pub id: u64,

    #[serde(default)]
    pub optional_field: Option<String>,

    #[serde(skip_serializing_if = "Option::is_none")]
    pub maybe_present: Option<i32>,
}

Pattern Matching

Use match for Enums

match result {
    Ok(data) => process(data),
    Err(KillmailError::NotFound) => warn!("Not found"),
    Err(KillmailError::RateLimited) => retry_later(),
    Err(e) => error!("Unexpected: {}", e),
}

Use if let for Single Patterns

// When you only care about one variant
if let Some(ship_name) = get_ship_name(type_id) {
    info!("Ship: {}", ship_name);
}

// Instead of verbose match
match get_ship_name(type_id) {
    Some(name) => info!("Ship: {}", name),
    None => {},
}

Iterators and Combinators

Prefer Iterator Methods Over Loops

// GOOD: Functional style
let high_value_kills: Vec<_> = killmails
    .iter()
    .filter(|k| k.zkb.total_value > 1_000_000_000.0)
    .collect();

// Also good when more readable
let mut results = Vec::new();
for km in killmails {
    if km.zkb.total_value > 1_000_000_000.0 {
        results.push(km);
    }
}

Common Combinators

// Transform
let names: Vec<String> = items.iter().map(|i| i.name.clone()).collect();

// Filter + Transform
let valid: Vec<_> = items.iter()
    .filter_map(|i| i.optional_field.as_ref())
    .collect();

// Find single item
let found = items.iter().find(|i| i.id == target_id);

// Check condition
let has_caps = attackers.iter().any(|a| is_capital(a.ship_type_id));

Logging with Tracing

use tracing::{info, warn, error, debug};

// Structured logging
info!(kill_id = %kill_id, "Processing killmail");
warn!(guild_id = %guild_id, "No subscriptions found");
error!(error = %e, "Failed to send message");

// With spans for context
let span = tracing::info_span!("process_kill", kill_id = %kill_id);
let _guard = span.enter();

Project-Specific Conventions

This Codebase Uses

  1. Serenity 0.11 for Discord - see serenity-discord-bot skill
  2. Reqwest for HTTP with rustls-tls
  3. Moka for caching
  4. Config files as JSON in config/ directory

File Organization

src/
  main.rs          # Entry point
  lib.rs           # Core logic, run() function
  config.rs        # Configuration loading/saving
  models.rs        # Data structures (Killmail, etc.)
  processor.rs     # Killmail → subscription matching
  discord_bot.rs   # Discord event handling
  esi.rs           # EVE ESI API client
  redis_q.rs       # zkillboard RedisQ listener
  commands/        # Discord slash commands
    mod.rs
    subscribe.rs
    unsubscribe.rs
    diag.rs

Quick Reference

| Pattern | Use For | |---------|---------| | Result<T, E> | Operations that can fail | | Option<T> | Values that may not exist | | ? operator | Clean error propagation | | Arc<T> | Shared ownership across threads | | RwLock<T> | Mutable shared state | | #[derive(...)] | Auto-implement common traits | | .iter().filter().map() | Transform collections |


Reference Files