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.
$ Installer
git clone https://github.com/ocn/zk-activity /tmp/zk-activity && cp -r /tmp/zk-activity/.claude/skills/rust-dev-guidelines ~/.claude/skills/zk-activity// tip: Run this command in your terminal to install the skill
SKILL.md
name: rust-dev-guidelines description: 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.
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
- Serenity 0.11 for Discord - see serenity-discord-bot skill
- Reqwest for HTTP with
rustls-tls - Moka for caching
- 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
- resources/error-handling.md - Custom errors, thiserror, anyhow
- resources/async-patterns.md - Tokio channels, timeouts, select
- resources/testing.md - Unit tests, integration tests, mocking
Repository

ocn
Author
ocn/zk-activity/.claude/skills/rust-dev-guidelines
6
Stars
0
Forks
Updated4d ago
Added1w ago