Instruction file imported from rustic-ai/codeprism (
.cursor/rules/rust.mdc). Copyright stays with the author.
description: This rule provides comprehensive best practices for Rust development, covering code organization, common patterns, performance, security, testing, pitfalls, and tooling. It aims to guide developers in writing idiomatic, efficient, secure, and maintainable Rust code. globs: .rs,Cargo.toml,**/.rs,**/Cargo.toml alwaysApply: false
Rust Essentials - Must-Have Basics
Purpose: Core quality requirements for reliable Rust code generation. Use this for basic code generation tasks, learning Rust fundamentals, or when you need clean, safe code that compiles without warnings.
When to use: AI code generation, code reviews, teaching Rust basics, or any situation requiring solid foundation patterns.
Core Quality Requirements
CRITICAL: All generated Rust code MUST:
- Compile without warnings on stable Rust
- Pass
cargo clippy --deny warnings - Follow
rustfmtformatting standards - Use Rust 2021 edition features
- Include proper error handling (no production
unwrap())
Error Handling Fundamentals
Rule: Never use unwrap(), expect(), or panic!() in production code paths.
Why: These cause immediate program termination, making your application unreliable. Always return Result<T, E> for operations that can fail, allowing callers to decide how to handle errors.
Use Result<T, E> for all fallible operations:
// ✅ GOOD
pub fn read_config(path: &Path) -> Result<Config, ConfigError> {
let content = fs::read_to_string(path)?;
toml::from_str(&content).map_err(ConfigError::ParseError)
}
// ❌ BAD - never use unwrap() in production
pub fn read_config(path: &Path) -> Config {
let content = fs::read_to_string(path).unwrap();
toml::from_str(&content).unwrap()
}
Rule: Use doc test features to ensure examples remain accurate and demonstrate different scenarios.
Why: Doc tests are automatically run by cargo test, ensuring examples never become outdated. Use different doc test attributes to show various use cases and error conditions.
Run doc tests with: cargo test --doc or cargo test (includes all tests)
Doc test best practices:
/// Parses configuration from various sources.
///
/// # Examples
///
/// Basic usage:
/// ```
/// let config = parse_config("app.toml")?;
/// assert!(config.port > 0);
/// # Ok::<(), ConfigError>(())
/// ```
///
/// This example doesn't run but shows the API:
/// ```no_run
/// let config = parse_config("/etc/myapp/config.toml")?;
/// deploy_with_config(config);
/// # Ok::<(), Box<dyn std::error::Error>>(())
/// ```
///
/// Demonstrating error handling:
/// ```should_panic
/// let config = parse_config("nonexistent.toml").unwrap();
/// ```
///
/// Hidden setup code (lines starting with #):
/// ```
/// # use std::fs;
/// # fs::write("test.toml", "port = 8080").unwrap();
/// let config = parse_config("test.toml")?;
/// assert_eq!(config.port, 8080);
/// # fs::remove_file("test.toml").unwrap();
/// # Ok::<(), ConfigError>(())
/// ```
pub fn parse_config(path: &str) -> Result<Config, ConfigError> {
// Implementation
}
**Rule: Create specific error types instead of using generic errors.**
Why: Specific errors enable proper error handling by callers and provide better debugging information. Use `thiserror` to reduce boilerplate.
**Define custom error types:**
```rust
#[derive(Debug, thiserror::Error)]
pub enum ConfigError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
ParseError(#[from] toml::de::Error),
}
Rule: Use the ? operator to propagate errors up the call stack.
Why: The ? operator provides clean error propagation without nested match statements. It automatically converts errors using the From trait.
Use the ? operator for error propagation:
pub fn process_user_data(id: u32) -> Result<UserProfile, UserError> {
let user = database::find_user(id)?; // Propagates database errors
let profile = build_profile(&user)?; // Propagates profile errors
Ok(profile)
}
Documentation Standards
Rule: Every public function, struct, and module must have rustdoc comments with working code examples.
Why: Documentation is part of the API contract. Good docs prevent misuse, reduce support burden, and make your code maintainable. Code examples are automatically tested by cargo test, ensuring documentation stays accurate.
Every public item needs rustdoc with examples:
/// Represents a user in the system.
///
/// # Examples
///
/// Creating a valid user:
/// ```
/// let user = User::new("alice@example.com", "Alice Smith")?;
/// assert_eq!(user.email(), "alice@example.com");
/// assert_eq!(user.name(), "Alice Smith");
/// # Ok::<(), UserError>(())
/// ```
///
/// Handling invalid email:
/// ```should_panic
/// let user = User::new("invalid-email", "Alice Smith").unwrap();
/// ```
///
/// # Errors
/// Returns `UserError::InvalidEmail` if email format is invalid.
#[derive(Debug, Clone)]
pub struct User {
email: String,
name: String,
}
impl User {
/// Creates a new user with validated email.
///
/// # Examples
/// ```
/// use my_crate::User;
///
/// let user = User::new("bob@example.com", "Bob Jones")?;
/// assert!(user.email().contains("@"));
/// # Ok::<(), my_crate::UserError>(())
/// ```
pub fn new(email: impl Into<String>, name: impl Into<String>) -> Result<Self, UserError> {
let email = email.into();
validate_email(&email)?;
Ok(User {
email,
name: name.into(),
})
}
}
Basic Testing Requirements
Rule: Write unit tests for all public functions, covering success cases, error cases, and edge cases. Why: Tests prevent regressions, document expected behavior, and enable confident refactoring. Test names should clearly describe what is being tested.
Include unit tests for all public functions:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_user_creation_success() {
let user = User::new("alice@example.com", "Alice Smith").unwrap();
assert_eq!(user.email(), "alice@example.com");
assert_eq!(user.name(), "Alice Smith");
}
#[test]
fn test_user_creation_invalid_email() {
let result = User::new("invalid-email", "Alice Smith");
assert!(result.is_err());
assert!(matches!(result.unwrap_err(), UserError::InvalidEmail));
}
#[test]
fn test_edge_case_empty_name() {
let result = User::new("test@example.com", "");
assert!(result.is_err());
}
}
Struct and Enum Patterns
Rule: Use appropriate derive traits to automatically implement common functionality.
Why: Derive traits reduce boilerplate code, ensure consistent implementations, and prevent bugs that come from manual implementations of traits like Debug, Clone, and PartialEq.
Use appropriate derives:
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UserId(pub u64);
#[derive(Debug, Clone, PartialEq)]
pub struct User {
pub id: UserId,
pub email: String,
pub name: String,
}
#[derive(Debug, serde::Serialize, serde::Deserialize)]
pub struct UserCreateRequest {
pub email: String,
pub name: String,
}
Rule: Use enums to represent state and handle mutually exclusive conditions. Why: Enums make invalid states unrepresentable, provide exhaustive pattern matching, and make code more self-documenting. They're safer than constants or booleans for representing state.
Use enums for state representation:
#[derive(Debug, Clone, PartialEq)]
pub enum UserStatus {
Active,
Inactive,
Suspended { reason: String },
}
#[derive(Debug, Clone)]
pub enum UserError {
InvalidEmail,
NameTooShort,
EmailAlreadyExists,
}
Safety and Input Validation
Rule: Validate all external inputs before processing them. Why: Invalid inputs are the source of most security vulnerabilities and runtime errors. Validate early and fail fast with clear error messages.
Always validate external inputs:
pub fn validate_email(email: &str) -> Result<(), UserError> {
if email.is_empty() || !email.contains('@') || email.len() > 254 {
return Err(UserError::InvalidEmail);
}
Ok(())
}
pub fn validate_age(age: u32) -> Result<(), UserError> {
if age > 150 {
return Err(UserError::InvalidAge);
}
Ok(())
}
Rule: Use safe indexing methods instead of direct array/slice indexing.
Why: Direct indexing with [] can panic if the index is out of bounds. Use .get() to return Option<T> for safe access.
Use safe indexing:
// ✅ GOOD - safe indexing
pub fn get_first_item<T>(items: &[T]) -> Option<&T> {
items.get(0)
}
// ❌ BAD - can panic
pub fn get_first_item<T>(items: &[T]) -> &T {
&items[0]
}
Rule: Use checked arithmetic operations to prevent integer overflow.
Why: Integer overflow is undefined behavior in release builds and can lead to security vulnerabilities. Checked operations return Option or Result allowing you to handle overflow gracefully.
Handle integer operations safely:
pub fn calculate_total_price(quantity: u32, unit_price: u32) -> Result<u32, CalculationError> {
quantity
.checked_mul(unit_price)
.ok_or(CalculationError::Overflow)
}
Basic Performance Guidelines
Rule: Prefer borrowing (&T) over taking ownership (T) when you don't need to own the data. Why: Borrowing avoids unnecessary memory allocations and copying, leads to better performance, and allows the same data to be used by multiple functions without cloning.
Prefer borrowing over cloning:
// ✅ GOOD - uses references
pub fn format_user_info(user: &User) -> String {
format!("{} <{}>", user.name(), user.email())
}
// ❌ BAD - unnecessary cloning
pub fn format_user_info(user: User) -> String {
format!("{} <{}>", user.name(), user.email())
}
Rule: Choose data structures based on their performance characteristics and access patterns. Why: The right data structure can dramatically improve performance. HashMap for fast lookups, Vec for sequential access, HashSet for uniqueness checks, VecDeque for queue operations.
Choose appropriate data structures:
use std::collections::{HashMap, HashSet, VecDeque};
pub struct UserManager {
users: HashMap<UserId, User>, // Fast lookups
active_sessions: HashSet<UserId>, // Unique values
pending_requests: VecDeque<Request>, // Queue operations
}
Essential Cargo.toml
Rule: Configure basic project metadata and essential dependencies. Why: Proper configuration ensures reproducible builds and includes necessary dependencies for error handling and testing.
Use the comprehensive Cargo.toml template from project-setup.md - this section covers only the essential additions:
[dependencies]
thiserror = "1.0" # Error handling
serde = { version = "1.0", features = ["derive"] }
[dev-dependencies]
rstest = "0.18" # Better testing
Code Generation Checklist
Before outputting Rust code, verify:
- Compiles without warnings
- Uses Result<T, E> for fallible operations
- No unwrap() in production paths
- Public items have rustdoc comments with working examples
- Doc tests demonstrate both success and error cases
- Includes basic unit tests
- Uses appropriate derives
- Validates external inputs
- Follows naming conventions (snake_case, etc.)