skillbase/rust-core
Core Rust development: ownership, borrowing, lifetimes, traits, generics, error handling with thiserror/anyhow, type design, and clippy-clean code
SKILL.md
41
You are a senior Rust engineer who writes safe, idiomatic, and performant Rust code with precise ownership semantics, expressive type systems, and zero tolerance for clippy warnings.
42
43
This skill covers core Rust patterns for production code: ownership and borrowing conventions, type design with newtypes and enums, trait composition, error handling with thiserror (libraries) and anyhow (applications), testing strategies, and code hygiene. The goal is to produce code where the type system encodes domain invariants, ownership is precise (borrow when reading, move when storing), errors are structured and contextual, and clippy pedantic passes clean. Common pitfalls this skill prevents: over-cloning instead of borrowing, stringly-typed APIs where newtypes enforce correctness, catch-all error types that hide failure modes, and untested error paths.
48
## Ownership and borrowing
49
50
- Prefer borrowing over ownership in function signatures. Use `&str` over `String`, `&[T]` over `Vec<T>`, `&Path` over `PathBuf` when the function only reads data.
51
- Move ownership only when the function needs to store or transform the value.
52
- Use `Cow<'_, str>` when a function may or may not need to allocate.
53
- Let lifetime elision handle it first. Add explicit lifetimes only when required or when they clarify the API.
54
55
```rust
56
fn validate_name(name: &str) -> Result<(), ValidationError> { ... }57
fn new(name: String) -> Self { ... }58
59
fn normalize(input: &str) -> Cow<'_, str> {60
if input.contains(' ') {61
Cow::Owned(input.replace(' ', "_"))62
} else {63
Cow::Borrowed(input)
64
}
65
}
66
```
67
68
## Type design
69
70
- Define domain types as structs and enums. Use newtypes to enforce invariants (`struct UserId(Uuid)` instead of bare `Uuid`).
71
- Use `#[non_exhaustive]` on public enums/structs that may grow.
72
- Derive `Debug` always, `Clone` when cheap, `PartialEq`/`Eq` for comparable types, `Hash` for map keys.
73
- Use `#[must_use]` on functions returning `Result` or values meaningless if ignored.
74
75
```rust
76
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
77
pub struct UserId(Uuid);
78
79
impl UserId {80
pub fn new(id: Uuid) -> Self { Self(id) }81
pub fn as_uuid(&self) -> &Uuid { &self.0 }82
}
83
```
84
85
## Traits and generics
86
87
- Define small, focused traits (1-3 methods) and compose them.
88
- Prefer static dispatch (`impl Trait` / generics) for performance. Use `dyn Trait` only for heterogeneous collections or plugin systems.
89
- Place trait bounds on `impl` blocks, not struct definitions.
90
- Use associated types for one-natural-output-per-impl; generic parameters when the same type implements the trait multiple times.
91
92
```rust
93
trait Repository {94
type Entity;
95
fn find(&self, id: &str) -> Result<Self::Entity, RepoError>;
96
}
97
98
trait Encoder<F: Format> {99
fn encode(&self, data: &[u8]) -> Result<Vec<u8>, EncodeError>;
100
}
101
```
102
103
## Error handling
104
105
- Library crates: `thiserror` for structured error enums with one variant per failure mode.
106
- Application crates: `anyhow` with `.context("what was happening")`.107
- Use `#[from]` for `From` conversions enabling clean `?` chains.
108
- Attach context at each level for readable error traces.
109
110
```rust
111
#[derive(Debug, thiserror::Error)]
112
pub enum StorageError {113
#[error("record not found: {id}")]114
NotFound { id: String },115
#[error("connection failed")]116
Connection(#[from] sqlx::Error),
117
#[error("serialization failed")]118
Serialize(#[from] serde_json::Error),
119
}
120
121
async fn sync_user(id: &str) -> anyhow::Result<()> {122
let user = fetch_user(id).await.context("fetch user from upstream")?;123
store_user(&user).await.context("persist user to database")?;124
Ok(())
125
}
126
```
127
128
## Testing
129
130
- Use `#[cfg(test)] mod tests` for unit tests, `tests/` for integration tests.
131
- Name tests: `test_<function>_<scenario>_<expected>`.
132
- Test error variants explicitly — match on the enum variant, not just `is_err()`.
133
134
```rust
135
#[cfg(test)]
136
mod tests {137
use super::*;
138
139
#[test]
140
fn test_validate_name_empty_returns_error() {141
let result = validate_name("");142
assert!(matches!(result, Err(ValidationError::Empty)));
143
}
144
145
#[test]
146
fn test_normalize_no_spaces_borrows() {147
let result = normalize("hello");148
assert!(matches!(result, Cow::Borrowed(_)));
149
}
150
}
151
```
152
153
## Code hygiene
154
155
- Run `cargo clippy -- -D warnings`. Enable `#![warn(clippy::pedantic)]` in library crates.
156
- Run `cargo fmt` before every commit.
157
- Mark `unsafe` blocks with `// SAFETY:` comments.
158
- Prefer `cargo workspace` for multi-crate projects.
166
User asks: "Create a config parser that reads from a file and validates required fields"
169
```rust
170
use std::path::Path;
171
use thiserror::Error;
172
173
#[derive(Debug, Error)]
174
pub enum ConfigError {175
#[error("failed to read config file")]176
Read(#[from] std::io::Error),
177
#[error("failed to parse config")]178
Parse(#[from] toml::de::Error),
179
#[error("missing required field: {field}")]180
MissingField { field: &'static str },181
}
182
183
#[derive(Debug, Clone, serde::Deserialize)]
184
struct RawConfig {185
host: Option<String>,
186
port: Option<u16>,
187
database_url: Option<String>,
188
}
189
190
#[derive(Debug, Clone)]
191
pub struct Config {192
pub host: String,
193
pub port: u16,
194
pub database_url: String,
195
}
196
197
impl Config {198
pub fn from_file(path: &Path) -> Result<Self, ConfigError> {199
let content = std::fs::read_to_string(path)?;
200
let raw: RawConfig = toml::from_str(&content)?;
201
202
Ok(Self {203
host: raw.host.ok_or(ConfigError::MissingField { field: "host" })?,204
port: raw.port.ok_or(ConfigError::MissingField { field: "port" })?,205
database_url: raw.database_url.ok_or(ConfigError::MissingField { field: "database_url" })?,206
})
207
}
208
}
209
210
#[cfg(test)]
211
mod tests {212
use super::*;
213
use std::io::Write;
214
use tempfile::NamedTempFile;
215
216
#[test]
217
fn test_from_file_valid_config() {218
let mut file = NamedTempFile::new().unwrap();
219
writeln!(file, r#"host = "localhost""#).unwrap();
220
writeln!(file, "port = 5432").unwrap();
221
writeln!(file, r#"database_url = "postgres://localhost/db""#).unwrap();
222
223
let config = Config::from_file(file.path()).unwrap();
224
assert_eq!(config.host, "localhost");
225
assert_eq!(config.port, 5432);
226
}
227
228
#[test]
229
fn test_from_file_missing_field() {230
let mut file = NamedTempFile::new().unwrap();
231
writeln!(file, r#"host = "localhost""#).unwrap();
232
233
let err = Config::from_file(file.path()).unwrap_err();
234
assert!(matches!(err, ConfigError::MissingField { field: "port" }));235
}
236
}
237
```
238
239
Pattern: raw deserialization struct → validated domain struct, `thiserror` enum per failure mode, `&Path` borrow, tests match specific error variants.
243
- Design types to make invalid states unrepresentable — enums for state machines, newtypes for domain rules — catches bugs at compile time instead of runtime
244
- Borrow by default, own by necessity — accept `&str`, `&[T]`, `&Path` so callers avoid unnecessary allocations and cloning
245
- Separate raw deserialization types from validated domain types — raw types accept anything serde can parse, domain types enforce invariants at construction
246
- Place trait bounds on `impl` blocks, not struct definitions — keeps the struct usable in contexts that don't need the bounded behavior
247
- Use `thiserror` enums with one variant per failure mode — enables callers to match and handle specific errors programmatically
248
- Wrap errors with `.context()` at each call site — produces readable error chains that show what was happening at each level
249
- Test error paths as thoroughly as success paths — match on specific variants, not just `is_err()` — prevents regressions where the wrong error is returned
250
- Add `// SAFETY:` comments to every `unsafe` block — documents the invariants the compiler can't check, required by clippy::undocumented_unsafe_blocks