mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-03-30 13:43:26 +08:00
* feat(agents): add Rust language support — reviewer, build resolver, patterns, and testing
Add Rust-specific agents and skills following the established Go/Kotlin pattern:
- agents/rust-reviewer.md: ownership, lifetimes, unsafe audit, clippy, error handling
- agents/rust-build-resolver.md: cargo build errors, borrow checker, dependency resolution
- skills/rust-patterns/SKILL.md: idiomatic Rust patterns and best practices
- skills/rust-testing/SKILL.md: TDD, unit/integration/async/property-based testing
* fix(agents): correct Rust examples for accuracy and consistency
- unsafe fn: add inner unsafe {} block for Rust 2024 edition compliance
- edition: update from 2021 to 2024 as current default
- rstest: add missing fixture import
- mockall: add missing predicate::eq import
- concurrency: use sync_channel (bounded) and expect() over unwrap()
to align with rust-reviewer's HIGH-priority review checks
* fix(skills): correct compilation issues in Rust code examples
- collect: add .copied() for &str iterator into String
- tokio import: remove unused sleep, keep Duration
- async test: add missing Duration import
* fix(skills): move --no-fail-fast before test-binary args
--no-fail-fast is a Cargo option, not a test binary flag.
Placing it after -- forwards it to the test harness where it is
unrecognized.
* fix(agents): distinguish missing cargo-audit from real audit failures
Check if cargo-audit is installed before running it, so actual
vulnerability findings are not suppressed by the fallback message.
* fix: address automated review findings across all Rust files
- build-resolver: prefer scoped cargo update over full refresh
- testing: add Cargo.toml bench config with harness = false for criterion
- testing: condense TDD example to stay under 500-line limit
- patterns: use expect() over unwrap() on JoinHandle for consistency
- patterns: add explicit lifetime to unsafe FFI return reference
- reviewer: replace misleading "string interpolation" with concrete alternatives
* fix: align with CONTRIBUTING.md conventions
- skills: rename "When to Activate" to "When to Use" per template
- reviewer: add cargo check gate before starting review
* fix(agents): guard cargo-audit and cargo-deny with availability checks
Match the pattern used in rust-build-resolver to avoid command-not-found
errors when optional tools are not installed.
* fix: address second round of automated review findings
- testing: split TDD example into separate code blocks to avoid
duplicate fn definition in single block
- build-resolver/reviewer: use if/then/else instead of && ... ||
chaining for cargo-audit/deny to avoid masking real failures
- build-resolver: add MSRV caveat to edition upgrade guidance
* feat: add Rust slash commands for build, review, and test
Add commands/rust-build.md, commands/rust-review.md, and
commands/rust-test.md to provide consistent user entrypoints
matching the existing Go and Kotlin command patterns.
* fix(commands): improve rust-build accuracy and tone
- Restructure-first borrow fix example instead of clone-first
- Realistic cargo test output format (per-test lines, not per-file)
- Align "Parse Errors" step with actual resolver behavior
- Prefer restructuring over cloning in common errors table
* fix: address cubic-dev-ai review findings on commands
- Gate review on all automated checks, not just cargo check
- Use git diff HEAD~1 / git diff main...HEAD for PR file selection
- Fix #[must_use] guidance: Result is already must_use by type
- Remove error-masking fallback on cargo tree --duplicates
* fix: address remaining review findings across all bots
- Add rust-reviewer and rust-build-resolver to AGENTS.md registry
- Update agent count from 16 to 18
- Mark parse_config doctest as no_run (body is todo!())
- Add "How It Works" section to both Rust skills
- Replace cargo install with taiki-e/install-action in CI snippet
- Trim tarpaulin section to stay under 500-line limit
* fix(agents): align rust-reviewer invocation with command spec
- Use git diff HEAD~1 / main...HEAD instead of bare git diff
- Add cargo test as explicit step before review begins
* fix(skills): address cubic review on patterns and testing
- Remove Tokio-specific language from How It Works summary
- Add cargo-llvm-cov install note in coverage section
- Revert no_run on doctest examples (illustrative code, not compiled)
* fix(skills): use expect on thread join for consistency
Replace handle.join().unwrap() with .expect("worker thread panicked")
to match the .expect("mutex poisoned") pattern used above.
* fix(agents): gate review on all automated checks, not just cargo check
Consolidate check/clippy/fmt/test into a single gate step that
stops and reports if any fail, matching the command spec.
* fix(skills): replace unwrap with expect in channel example
Use .expect("receiver disconnected") on tx.send() for consistency
with the .expect() convention used in all other concurrency examples.
* fix: address final review round — OpenCode mirrors, counts, examples
- Add .opencode/commands/rust-{build,review,test}.md mirrors
- Add .opencode/prompts/agents/rust-{build-resolver,reviewer}.txt mirrors
- Fix AGENTS.md count to 20 (add missing kotlin agents to table)
- Fix review example: all checks pass (consistent with gate policy)
- Replace should_panic doctest with is_err() (consistent with best practices)
- Trim testing commands to stay at 500-line limit
* fix: address cubic and greptile review on OpenCode files and agents
- Fix crate::module import guidance (internal path, not Cargo.toml)
- Add cargo fmt --check to verification steps
- Fix TDD GREEN example to handle error path (validate(input)?)
- Scope .context() guidance to anyhow/eyre application code
- Update command count from 40 to 51
- Add tokio channel variants to unbounded channel warning
- Preserve JoinError context in spawned task panic message
* fix: stale command count, channel guidance, cargo tree fallback
- Fix stale command count in Project Structure section (40→51)
- Clarify unbounded channel rule: context-appropriate bounded alternatives
- Remove dead cargo tree fallback (exits 0 even with no duplicates)
- Sync OpenCode reviewer mirror with tokio channel coverage
501 lines
13 KiB
Markdown
501 lines
13 KiB
Markdown
---
|
|
name: rust-patterns
|
|
description: Idiomatic Rust patterns, ownership, error handling, traits, concurrency, and best practices for building safe, performant applications.
|
|
origin: ECC
|
|
---
|
|
|
|
# Rust Development Patterns
|
|
|
|
Idiomatic Rust patterns and best practices for building safe, performant, and maintainable applications.
|
|
|
|
## When to Use
|
|
|
|
- Writing new Rust code
|
|
- Reviewing Rust code
|
|
- Refactoring existing Rust code
|
|
- Designing crate structure and module layout
|
|
|
|
## How It Works
|
|
|
|
This skill enforces idiomatic Rust conventions across six key areas: ownership and borrowing to prevent data races at compile time, `Result`/`?` error propagation with `thiserror` for libraries and `anyhow` for applications, enums and exhaustive pattern matching to make illegal states unrepresentable, traits and generics for zero-cost abstraction, safe concurrency via `Arc<Mutex<T>>`, channels, and async/await, and minimal `pub` surfaces organized by domain.
|
|
|
|
## Core Principles
|
|
|
|
### 1. Ownership and Borrowing
|
|
|
|
Rust's ownership system prevents data races and memory bugs at compile time.
|
|
|
|
```rust
|
|
// Good: Pass references when you don't need ownership
|
|
fn process(data: &[u8]) -> usize {
|
|
data.len()
|
|
}
|
|
|
|
// Good: Take ownership only when you need to store or consume
|
|
fn store(data: Vec<u8>) -> Record {
|
|
Record { payload: data }
|
|
}
|
|
|
|
// Bad: Cloning unnecessarily to avoid borrow checker
|
|
fn process_bad(data: &Vec<u8>) -> usize {
|
|
let cloned = data.clone(); // Wasteful — just borrow
|
|
cloned.len()
|
|
}
|
|
```
|
|
|
|
|
|
### Use `Cow` for Flexible Ownership
|
|
|
|
```rust
|
|
use std::borrow::Cow;
|
|
|
|
fn normalize(input: &str) -> Cow<'_, str> {
|
|
if input.contains(' ') {
|
|
Cow::Owned(input.replace(' ', "_"))
|
|
} else {
|
|
Cow::Borrowed(input) // Zero-cost when no mutation needed
|
|
}
|
|
}
|
|
```
|
|
|
|
## Error Handling
|
|
|
|
### Use `Result` and `?` — Never `unwrap()` in Production
|
|
|
|
```rust
|
|
// Good: Propagate errors with context
|
|
use anyhow::{Context, Result};
|
|
|
|
fn load_config(path: &str) -> Result<Config> {
|
|
let content = std::fs::read_to_string(path)
|
|
.with_context(|| format!("failed to read config from {path}"))?;
|
|
let config: Config = toml::from_str(&content)
|
|
.with_context(|| format!("failed to parse config from {path}"))?;
|
|
Ok(config)
|
|
}
|
|
|
|
// Bad: Panics on error
|
|
fn load_config_bad(path: &str) -> Config {
|
|
let content = std::fs::read_to_string(path).unwrap(); // Panics!
|
|
toml::from_str(&content).unwrap()
|
|
}
|
|
```
|
|
|
|
### Library Errors with `thiserror`, Application Errors with `anyhow`
|
|
|
|
```rust
|
|
// Library code: structured, typed errors
|
|
use thiserror::Error;
|
|
|
|
#[derive(Debug, Error)]
|
|
pub enum StorageError {
|
|
#[error("record not found: {id}")]
|
|
NotFound { id: String },
|
|
#[error("connection failed")]
|
|
Connection(#[from] std::io::Error),
|
|
#[error("invalid data: {0}")]
|
|
InvalidData(String),
|
|
}
|
|
|
|
// Application code: flexible error handling
|
|
use anyhow::{bail, Result};
|
|
|
|
fn run() -> Result<()> {
|
|
let config = load_config("app.toml")?;
|
|
if config.workers == 0 {
|
|
bail!("worker count must be > 0");
|
|
}
|
|
Ok(())
|
|
}
|
|
```
|
|
|
|
### `Option` Combinators Over Nested Matching
|
|
|
|
```rust
|
|
// Good: Combinator chain
|
|
fn find_user_email(users: &[User], id: u64) -> Option<String> {
|
|
users.iter()
|
|
.find(|u| u.id == id)
|
|
.map(|u| u.email.clone())
|
|
}
|
|
|
|
// Bad: Deeply nested matching
|
|
fn find_user_email_bad(users: &[User], id: u64) -> Option<String> {
|
|
match users.iter().find(|u| u.id == id) {
|
|
Some(user) => match &user.email {
|
|
email => Some(email.clone()),
|
|
},
|
|
None => None,
|
|
}
|
|
}
|
|
```
|
|
|
|
## Enums and Pattern Matching
|
|
|
|
### Model States as Enums
|
|
|
|
```rust
|
|
// Good: Impossible states are unrepresentable
|
|
enum ConnectionState {
|
|
Disconnected,
|
|
Connecting { attempt: u32 },
|
|
Connected { session_id: String },
|
|
Failed { reason: String, retries: u32 },
|
|
}
|
|
|
|
fn handle(state: &ConnectionState) {
|
|
match state {
|
|
ConnectionState::Disconnected => connect(),
|
|
ConnectionState::Connecting { attempt } if *attempt > 3 => abort(),
|
|
ConnectionState::Connecting { .. } => wait(),
|
|
ConnectionState::Connected { session_id } => use_session(session_id),
|
|
ConnectionState::Failed { retries, .. } if *retries < 5 => retry(),
|
|
ConnectionState::Failed { reason, .. } => log_failure(reason),
|
|
}
|
|
}
|
|
```
|
|
|
|
### Exhaustive Matching — No Catch-All for Business Logic
|
|
|
|
```rust
|
|
// Good: Handle every variant explicitly
|
|
match command {
|
|
Command::Start => start_service(),
|
|
Command::Stop => stop_service(),
|
|
Command::Restart => restart_service(),
|
|
// Adding a new variant forces handling here
|
|
}
|
|
|
|
// Bad: Wildcard hides new variants
|
|
match command {
|
|
Command::Start => start_service(),
|
|
_ => {} // Silently ignores Stop, Restart, and future variants
|
|
}
|
|
```
|
|
|
|
## Traits and Generics
|
|
|
|
### Accept Generics, Return Concrete Types
|
|
|
|
```rust
|
|
// Good: Generic input, concrete output
|
|
fn read_all(reader: &mut impl Read) -> std::io::Result<Vec<u8>> {
|
|
let mut buf = Vec::new();
|
|
reader.read_to_end(&mut buf)?;
|
|
Ok(buf)
|
|
}
|
|
|
|
// Good: Trait bounds for multiple constraints
|
|
fn process<T: Display + Send + 'static>(item: T) -> String {
|
|
format!("processed: {item}")
|
|
}
|
|
```
|
|
|
|
### Trait Objects for Dynamic Dispatch
|
|
|
|
```rust
|
|
// Use when you need heterogeneous collections or plugin systems
|
|
trait Handler: Send + Sync {
|
|
fn handle(&self, request: &Request) -> Response;
|
|
}
|
|
|
|
struct Router {
|
|
handlers: Vec<Box<dyn Handler>>,
|
|
}
|
|
|
|
// Use generics when you need performance (monomorphization)
|
|
fn fast_process<H: Handler>(handler: &H, request: &Request) -> Response {
|
|
handler.handle(request)
|
|
}
|
|
```
|
|
|
|
### Newtype Pattern for Type Safety
|
|
|
|
```rust
|
|
// Good: Distinct types prevent mixing up arguments
|
|
struct UserId(u64);
|
|
struct OrderId(u64);
|
|
|
|
fn get_order(user: UserId, order: OrderId) -> Result<Order> {
|
|
// Can't accidentally swap user and order IDs
|
|
todo!()
|
|
}
|
|
|
|
// Bad: Easy to swap arguments
|
|
fn get_order_bad(user_id: u64, order_id: u64) -> Result<Order> {
|
|
todo!()
|
|
}
|
|
```
|
|
|
|
## Structs and Data Modeling
|
|
|
|
### Builder Pattern for Complex Construction
|
|
|
|
```rust
|
|
struct ServerConfig {
|
|
host: String,
|
|
port: u16,
|
|
max_connections: usize,
|
|
}
|
|
|
|
impl ServerConfig {
|
|
fn builder(host: impl Into<String>, port: u16) -> ServerConfigBuilder {
|
|
ServerConfigBuilder { host: host.into(), port, max_connections: 100 }
|
|
}
|
|
}
|
|
|
|
struct ServerConfigBuilder { host: String, port: u16, max_connections: usize }
|
|
|
|
impl ServerConfigBuilder {
|
|
fn max_connections(mut self, n: usize) -> Self { self.max_connections = n; self }
|
|
fn build(self) -> ServerConfig {
|
|
ServerConfig { host: self.host, port: self.port, max_connections: self.max_connections }
|
|
}
|
|
}
|
|
|
|
// Usage: ServerConfig::builder("localhost", 8080).max_connections(200).build()
|
|
```
|
|
|
|
## Iterators and Closures
|
|
|
|
### Prefer Iterator Chains Over Manual Loops
|
|
|
|
```rust
|
|
// Good: Declarative, lazy, composable
|
|
let active_emails: Vec<String> = users.iter()
|
|
.filter(|u| u.is_active)
|
|
.map(|u| u.email.clone())
|
|
.collect();
|
|
|
|
// Bad: Imperative accumulation
|
|
let mut active_emails = Vec::new();
|
|
for user in &users {
|
|
if user.is_active {
|
|
active_emails.push(user.email.clone());
|
|
}
|
|
}
|
|
```
|
|
|
|
### Use `collect()` with Type Annotation
|
|
|
|
```rust
|
|
// Collect into different types
|
|
let names: Vec<_> = items.iter().map(|i| &i.name).collect();
|
|
let lookup: HashMap<_, _> = items.iter().map(|i| (i.id, i)).collect();
|
|
let combined: String = parts.iter().copied().collect();
|
|
|
|
// Collect Results — short-circuits on first error
|
|
let parsed: Result<Vec<i32>, _> = strings.iter().map(|s| s.parse()).collect();
|
|
```
|
|
|
|
## Concurrency
|
|
|
|
### `Arc<Mutex<T>>` for Shared Mutable State
|
|
|
|
```rust
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
let counter = Arc::new(Mutex::new(0));
|
|
let handles: Vec<_> = (0..10).map(|_| {
|
|
let counter = Arc::clone(&counter);
|
|
std::thread::spawn(move || {
|
|
let mut num = counter.lock().expect("mutex poisoned");
|
|
*num += 1;
|
|
})
|
|
}).collect();
|
|
|
|
for handle in handles {
|
|
handle.join().expect("worker thread panicked");
|
|
}
|
|
```
|
|
|
|
### Channels for Message Passing
|
|
|
|
```rust
|
|
use std::sync::mpsc;
|
|
|
|
let (tx, rx) = mpsc::sync_channel(16); // Bounded channel with backpressure
|
|
|
|
for i in 0..5 {
|
|
let tx = tx.clone();
|
|
std::thread::spawn(move || {
|
|
tx.send(format!("message {i}")).expect("receiver disconnected");
|
|
});
|
|
}
|
|
drop(tx); // Close sender so rx iterator terminates
|
|
|
|
for msg in rx {
|
|
println!("{msg}");
|
|
}
|
|
```
|
|
|
|
### Async with Tokio
|
|
|
|
```rust
|
|
use tokio::time::Duration;
|
|
|
|
async fn fetch_with_timeout(url: &str) -> Result<String> {
|
|
let response = tokio::time::timeout(
|
|
Duration::from_secs(5),
|
|
reqwest::get(url),
|
|
)
|
|
.await
|
|
.context("request timed out")?
|
|
.context("request failed")?;
|
|
|
|
response.text().await.context("failed to read body")
|
|
}
|
|
|
|
// Spawn concurrent tasks
|
|
async fn fetch_all(urls: Vec<String>) -> Vec<Result<String>> {
|
|
let handles: Vec<_> = urls.into_iter()
|
|
.map(|url| tokio::spawn(async move {
|
|
fetch_with_timeout(&url).await
|
|
}))
|
|
.collect();
|
|
|
|
let mut results = Vec::with_capacity(handles.len());
|
|
for handle in handles {
|
|
results.push(handle.await.unwrap_or_else(|e| panic!("spawned task panicked: {e}")));
|
|
}
|
|
results
|
|
}
|
|
```
|
|
|
|
## Unsafe Code
|
|
|
|
### When Unsafe Is Acceptable
|
|
|
|
```rust
|
|
// Acceptable: FFI boundary with documented invariants (Rust 2024+)
|
|
/// # Safety
|
|
/// `ptr` must be a valid, aligned pointer to an initialized `Widget`.
|
|
unsafe fn widget_from_raw<'a>(ptr: *const Widget) -> &'a Widget {
|
|
// SAFETY: caller guarantees ptr is valid and aligned
|
|
unsafe { &*ptr }
|
|
}
|
|
|
|
// Acceptable: Performance-critical path with proof of correctness
|
|
// SAFETY: index is always < len due to the loop bound
|
|
unsafe { slice.get_unchecked(index) }
|
|
```
|
|
|
|
### When Unsafe Is NOT Acceptable
|
|
|
|
```rust
|
|
// Bad: Using unsafe to bypass borrow checker
|
|
// Bad: Using unsafe for convenience
|
|
// Bad: Using unsafe without a Safety comment
|
|
// Bad: Transmuting between unrelated types
|
|
```
|
|
|
|
## Module System and Crate Structure
|
|
|
|
### Organize by Domain, Not by Type
|
|
|
|
```text
|
|
my_app/
|
|
├── src/
|
|
│ ├── main.rs
|
|
│ ├── lib.rs
|
|
│ ├── auth/ # Domain module
|
|
│ │ ├── mod.rs
|
|
│ │ ├── token.rs
|
|
│ │ └── middleware.rs
|
|
│ ├── orders/ # Domain module
|
|
│ │ ├── mod.rs
|
|
│ │ ├── model.rs
|
|
│ │ └── service.rs
|
|
│ └── db/ # Infrastructure
|
|
│ ├── mod.rs
|
|
│ └── pool.rs
|
|
├── tests/ # Integration tests
|
|
├── benches/ # Benchmarks
|
|
└── Cargo.toml
|
|
```
|
|
|
|
### Visibility — Expose Minimally
|
|
|
|
```rust
|
|
// Good: pub(crate) for internal sharing
|
|
pub(crate) fn validate_input(input: &str) -> bool {
|
|
!input.is_empty()
|
|
}
|
|
|
|
// Good: Re-export public API from lib.rs
|
|
pub mod auth;
|
|
pub use auth::AuthMiddleware;
|
|
|
|
// Bad: Making everything pub
|
|
pub fn internal_helper() {} // Should be pub(crate) or private
|
|
```
|
|
|
|
## Tooling Integration
|
|
|
|
### Essential Commands
|
|
|
|
```bash
|
|
# Build and check
|
|
cargo build
|
|
cargo check # Fast type checking without codegen
|
|
cargo clippy # Lints and suggestions
|
|
cargo fmt # Format code
|
|
|
|
# Testing
|
|
cargo test
|
|
cargo test -- --nocapture # Show println output
|
|
cargo test --lib # Unit tests only
|
|
cargo test --test integration # Integration tests only
|
|
|
|
# Dependencies
|
|
cargo audit # Security audit
|
|
cargo tree # Dependency tree
|
|
cargo update # Update dependencies
|
|
|
|
# Performance
|
|
cargo bench # Run benchmarks
|
|
```
|
|
|
|
## Quick Reference: Rust Idioms
|
|
|
|
| Idiom | Description |
|
|
|-------|-------------|
|
|
| Borrow, don't clone | Pass `&T` instead of cloning unless ownership is needed |
|
|
| Make illegal states unrepresentable | Use enums to model valid states only |
|
|
| `?` over `unwrap()` | Propagate errors, never panic in library/production code |
|
|
| Parse, don't validate | Convert unstructured data to typed structs at the boundary |
|
|
| Newtype for type safety | Wrap primitives in newtypes to prevent argument swaps |
|
|
| Prefer iterators over loops | Declarative chains are clearer and often faster |
|
|
| `#[must_use]` on Results | Ensure callers handle return values |
|
|
| `Cow` for flexible ownership | Avoid allocations when borrowing suffices |
|
|
| Exhaustive matching | No wildcard `_` for business-critical enums |
|
|
| Minimal `pub` surface | Use `pub(crate)` for internal APIs |
|
|
|
|
## Anti-Patterns to Avoid
|
|
|
|
```rust
|
|
// Bad: .unwrap() in production code
|
|
let value = map.get("key").unwrap();
|
|
|
|
// Bad: .clone() to satisfy borrow checker without understanding why
|
|
let data = expensive_data.clone();
|
|
process(&original, &data);
|
|
|
|
// Bad: Using String when &str suffices
|
|
fn greet(name: String) { /* should be &str */ }
|
|
|
|
// Bad: Box<dyn Error> in libraries (use thiserror instead)
|
|
fn parse(input: &str) -> Result<Data, Box<dyn std::error::Error>> { todo!() }
|
|
|
|
// Bad: Ignoring must_use warnings
|
|
let _ = validate(input); // Silently discarding a Result
|
|
|
|
// Bad: Blocking in async context
|
|
async fn bad_async() {
|
|
std::thread::sleep(Duration::from_secs(1)); // Blocks the executor!
|
|
// Use: tokio::time::sleep(Duration::from_secs(1)).await;
|
|
}
|
|
```
|
|
|
|
**Remember**: If it compiles, it's probably correct — but only if you avoid `unwrap()`, minimize `unsafe`, and let the type system work for you.
|