Skip to content

Commit 37cc2ab

Browse files
authored
Tweak Claude (#832)
Help us write better Rust code.
1 parent bce0e7a commit 37cc2ab

4 files changed

Lines changed: 382 additions & 84 deletions

File tree

.claude/skills/rust/SKILL.md

Lines changed: 373 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,373 @@
1+
---
2+
name: rust
3+
description: Rust coding best practices for idiomatic, efficient, and maintainable code. Use when writing Rust code, reviewing code, or learning Rust patterns.
4+
allowed-tools: Read, Edit, Write, Bash, Grep, Glob, Task
5+
---
6+
7+
# Rust Best Practices
8+
9+
Guidelines for writing idiomatic, efficient, and maintainable Rust code.
10+
11+
## Core Principles
12+
13+
1. **Leverage the type system** - Make invalid states unrepresentable
14+
2. **Prefer compile-time checks** - Catch errors before runtime
15+
3. **Be explicit about ownership** - Don't fight the borrow checker
16+
4. **Write code that passes fmt/clippy first** - Not after fixing
17+
18+
## Code health
19+
20+
1. Prefer smaller files, refactor large files into multi-file modules
21+
2. Refactor large modules into several
22+
3. Prefer re-usable functions. Don't create separate functions if re-use is not planned.
23+
24+
## Error Handling
25+
26+
Use `thiserror`
27+
28+
```rust
29+
use thiserror::Error;
30+
31+
#[derive(Error, Debug)]
32+
pub enum ConfigError {
33+
#[error("Failed to read config: {0}")]
34+
Io(#[from] std::io::Error),
35+
36+
#[error("Failed to parse config: {0}")]
37+
Parse(#[from] toml::de::Error),
38+
39+
#[error("Invalid configuration: {message}")]
40+
Invalid { message: String },
41+
}
42+
```
43+
44+
### Never Use `.unwrap()`
45+
46+
```rust
47+
// BAD
48+
let value = map.get("key").unwrap();
49+
50+
// GOOD
51+
let value = map.get("key").ok_or_else(|| Error::MissingKey("key"))?;
52+
53+
// GOOD (when None is truly impossible)
54+
let value = map.get("key").expect("key always present after init");
55+
```
56+
57+
## Ownership & Borrowing
58+
59+
### Prefer Borrowing Over Cloning
60+
61+
```rust
62+
// BAD - unnecessary clone
63+
fn process(data: String) { ... }
64+
process(my_string.clone());
65+
66+
// GOOD - borrow when possible
67+
fn process(data: &str) { ... }
68+
process(&my_string);
69+
```
70+
71+
### Use `Cow` for Flexible Ownership
72+
73+
```rust
74+
use std::borrow::Cow;
75+
76+
fn process(data: Cow<'_, str>) -> Cow<'_, str> {
77+
if data.contains("bad") {
78+
Cow::Owned(data.replace("bad", "good"))
79+
} else {
80+
data // No allocation if unchanged
81+
}
82+
}
83+
```
84+
85+
### Return Owned Data from Constructors
86+
87+
```rust
88+
// GOOD - clear ownership
89+
impl User {
90+
pub fn new(name: impl Into<String>) -> Self {
91+
Self { name: name.into() }
92+
}
93+
}
94+
```
95+
96+
## API Design
97+
98+
### Builder Pattern for Complex Configuration
99+
100+
```rust
101+
#[derive(Default)]
102+
pub struct ServerBuilder {
103+
host: Option<String>,
104+
port: Option<u16>,
105+
timeout: Option<Duration>,
106+
}
107+
108+
impl ServerBuilder {
109+
pub fn host(mut self, host: impl Into<String>) -> Self {
110+
self.host = Some(host.into());
111+
self
112+
}
113+
114+
pub fn port(mut self, port: u16) -> Self {
115+
self.port = Some(port);
116+
self
117+
}
118+
119+
pub fn build(self) -> Result<Server, ConfigError> {
120+
Ok(Server {
121+
host: self.host.unwrap_or_else(|| "localhost".into()),
122+
port: self.port.ok_or(ConfigError::MissingPort)?,
123+
timeout: self.timeout.unwrap_or(Duration::from_secs(30)),
124+
})
125+
}
126+
}
127+
```
128+
129+
### Newtype Pattern for Type Safety
130+
131+
```rust
132+
// BAD - easy to mix up
133+
fn transfer(from: i64, to: i64, amount: i64) { ... }
134+
135+
// GOOD - compile-time safety
136+
pub struct AccountId(i64);
137+
pub struct Amount(i64);
138+
139+
fn transfer(from: AccountId, to: AccountId, amount: Amount) { ... }
140+
```
141+
142+
### Use `#[must_use]` for Important Returns
143+
144+
```rust
145+
#[must_use]
146+
pub fn validate(&self) -> Result<(), ValidationError> {
147+
// ...
148+
}
149+
```
150+
151+
## Collections & Iterators
152+
153+
### Prefer Iterators Over Loops
154+
155+
```rust
156+
// BAD
157+
let mut results = Vec::new();
158+
for item in items {
159+
if item.is_valid() {
160+
results.push(item.transform());
161+
}
162+
}
163+
164+
// GOOD
165+
let results: Vec<_> = items
166+
.into_iter()
167+
.filter(|item| item.is_valid())
168+
.map(|item| item.transform())
169+
.collect();
170+
```
171+
172+
### Use `collect()` Type Inference
173+
174+
```rust
175+
// Collect into Vec
176+
let vec: Vec<_> = iter.collect();
177+
178+
// Collect into HashMap
179+
let map: HashMap<_, _> = iter.collect();
180+
181+
// Collect Results
182+
let results: Result<Vec<_>, _> = iter.collect();
183+
```
184+
185+
## Async Patterns
186+
187+
### Use `tokio` for Async Runtime
188+
189+
```rust
190+
#[tokio::main]
191+
async fn main() -> Result<()> {
192+
let result = fetch_data().await?;
193+
Ok(())
194+
}
195+
```
196+
197+
### Avoid Blocking in Async Code
198+
199+
```rust
200+
// BAD - blocks the runtime
201+
async fn bad() {
202+
std::thread::sleep(Duration::from_secs(1));
203+
}
204+
205+
// GOOD - async sleep
206+
async fn good() {
207+
tokio::time::sleep(Duration::from_secs(1)).await;
208+
}
209+
210+
// GOOD - spawn blocking for CPU-intensive work
211+
async fn compute() -> i32 {
212+
tokio::task::spawn_blocking(|| expensive_computation()).await.unwrap()
213+
}
214+
```
215+
216+
## Testing
217+
218+
### Unit Tests in Same File
219+
220+
```rust
221+
#[cfg(test)]
222+
mod tests {
223+
use super::*;
224+
225+
#[test]
226+
fn test_basic() {
227+
assert_eq!(add(1, 2), 3);
228+
}
229+
230+
#[test]
231+
fn test_edge_case() {
232+
assert!(validate("").is_err());
233+
}
234+
}
235+
```
236+
237+
### Integration Tests in `tests/`
238+
239+
```rust
240+
// tests/integration_test.rs
241+
use my_crate::public_api;
242+
243+
#[test]
244+
fn test_full_workflow() {
245+
let result = public_api::process("input");
246+
assert!(result.is_ok());
247+
}
248+
```
249+
250+
### Use `assert!` Macros Effectively
251+
252+
```rust
253+
assert!(condition);
254+
assert_eq!(left, right);
255+
assert_ne!(left, right);
256+
assert!(result.is_ok());
257+
assert!(result.is_err());
258+
assert_matches!(value, Pattern::Variant { .. });
259+
```
260+
261+
## Performance
262+
263+
### Avoid Premature Allocation
264+
265+
```rust
266+
// BAD - allocates even if not needed
267+
fn maybe_string() -> String {
268+
String::from("default")
269+
}
270+
271+
// GOOD - return static str when possible
272+
fn maybe_string() -> &'static str {
273+
"default"
274+
}
275+
```
276+
277+
### Use `Vec::with_capacity` for Known Sizes
278+
279+
```rust
280+
// BAD - multiple reallocations
281+
let mut vec = Vec::new();
282+
for i in 0..1000 {
283+
vec.push(i);
284+
}
285+
286+
// GOOD - single allocation
287+
let mut vec = Vec::with_capacity(1000);
288+
for i in 0..1000 {
289+
vec.push(i);
290+
}
291+
```
292+
293+
### Profile Before Optimizing
294+
295+
```bash
296+
cargo build --release
297+
cargo flamegraph # requires cargo-flamegraph
298+
```
299+
300+
## Module Organization
301+
302+
### Keep Modules Focused
303+
304+
```rust
305+
// src/lib.rs
306+
pub mod config;
307+
pub mod client;
308+
pub mod error;
309+
310+
// Re-export public API
311+
pub use config::Config;
312+
pub use client::Client;
313+
pub use error::Error;
314+
```
315+
316+
### Use `pub(crate)` for Internal APIs
317+
318+
```rust
319+
// Public to crate, not external users
320+
pub(crate) fn internal_helper() { ... }
321+
```
322+
323+
## Documentation
324+
325+
### Document Public APIs
326+
327+
```rust
328+
/// Creates a new client with the given configuration.
329+
///
330+
/// # Arguments
331+
///
332+
/// * `config` - The client configuration
333+
///
334+
/// # Errors
335+
///
336+
/// Returns an error if the configuration is invalid.
337+
///
338+
/// # Examples
339+
///
340+
/// ```
341+
/// let client = Client::new(Config::default())?;
342+
/// ```
343+
pub fn new(config: Config) -> Result<Self> {
344+
// ...
345+
}
346+
```
347+
348+
## Anti-Patterns to Avoid
349+
350+
| Anti-Pattern | Better Approach |
351+
|--------------|-----------------|
352+
| `.unwrap()` everywhere | Use `?` operator |
353+
| `clone()` to satisfy borrow checker | Restructure ownership |
354+
| `String` parameters | Use `&str` or `impl Into<String>` |
355+
| Boolean parameters | Use enums |
356+
| Long function bodies | Extract to smaller functions |
357+
| Deep nesting | Use early returns |
358+
| Magic numbers | Use named constants |
359+
360+
## Quick Reference
361+
362+
```bash
363+
# Quality gates
364+
cargo fmt -- --check && cargo clippy -- -D warnings && cargo test
365+
366+
# Common cargo commands
367+
cargo check # Fast syntax/type check
368+
cargo build # Debug build
369+
cargo build --release # Release build
370+
cargo nextest run # Run tests
371+
cargo doc --open # Generate and view docs
372+
cargo clippy --fix # Auto-fix lint issues
373+
```

AGENTS.md

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,2 @@
1-
# Repository Guidelines
21

3-
## Project Structure & Module Organization
4-
PgDog is a Rust workspace centred in `pgdog/` with the proxy under `pgdog/src` and end-to-end specs in `pgdog/tests`. Shared macros live in `pgdog-macros`, runtime extensions in `pgdog-plugin` plus `pgdog-plugin-build`, and loadable plugins under `plugins/`. Integration harnesses, Docker assets, and helper scripts sit in `integration/`, `docker/`, and `dev/`. Use `example.pgdog.toml` and `example.users.toml` as templates when adding sharded configs.
5-
6-
## Build, Test, and Development Commands
7-
Run `cargo check` for a quick type pass, and `cargo build` to compile local binaries; switch to `cargo build --release` for benchmarking or packaging. Start the development stack with `bash integration/dev-server.sh`, which provisions dependencies and runs `cargo watch` for live reloads. CI parity tests run via `cargo nextest run --test-threads=1 --no-fail-fast`. **Never invoke** `cargo test` directly—always use `cargo nextest run --test-threads=1 ...` for unit or integration suites so concurrency stays deterministic.
8-
9-
## Coding Style & Naming Conventions
10-
Follow Rust conventions: modules and functions in `snake_case`, types in `UpperCamelCase`, constants in `SCREAMING_SNAKE_CASE`. Keep modules under ~200 lines unless justified. Format with `cargo fmt` and lint using `cargo clippy --all-targets --all-features` before posting a PR.
11-
Prefer keeping `#[cfg(test)]` blocks at the end of a file; only place `#[cfg(test)]` imports directly beneath normal imports when that keeps the module tidy.
12-
13-
## Testing Guidelines
14-
Adhere to TDD—write the failing test first, implement minimally, then refactor. Co-locate unit tests with their crates, reserving heavier scenarios for `integration/` against the prepared-transaction Postgres stack. Invoke `cargo nextest run --test-threads=1 <test>` for focused iterations; gate Kerberos coverage behind `--features gssapi`. Do **not** run `cargo test`; Nextest with a single-thread budget is the required harness.
15-
16-
## Commit & Pull Request Guidelines
17-
Use concise, imperative commit subjects (e.g., "fix gssapi credential lookup") and describe user impact plus risk areas in PRs. Link relevant issues, attach logs or test output, and include config diffs or screenshots for operational changes. Keep PR scope tight and ensure linting, formatting, and tests pass locally before requesting review.
18-
19-
## Security & Configuration Tips
20-
Capture diagnostics before code changes, prefer configuration-driven fixes, and document new setup steps in `integration/README`. Never commit secrets; rely on the provided templates and environment variables when adjusting credentials or connection strings.
2+
Read `CLAUDE.md` and `.claude/skills/rust/SKILL.md`.

0 commit comments

Comments
 (0)