agentic-team-templates 0.9.2 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/index.js +36 -0
- package/src/index.test.js +9 -0
- package/templates/javascript-expert/.cursorrules/language-deep-dive.md +245 -0
- package/templates/javascript-expert/.cursorrules/node-patterns.md +184 -0
- package/templates/javascript-expert/.cursorrules/overview.md +130 -0
- package/templates/javascript-expert/.cursorrules/performance.md +203 -0
- package/templates/javascript-expert/.cursorrules/react-patterns.md +249 -0
- package/templates/javascript-expert/.cursorrules/testing.md +403 -0
- package/templates/javascript-expert/.cursorrules/tooling.md +176 -0
- package/templates/javascript-expert/CLAUDE.md +448 -0
- package/templates/rust-expert/.cursorrules/concurrency.md +250 -0
- package/templates/rust-expert/.cursorrules/ecosystem-and-tooling.md +299 -0
- package/templates/rust-expert/.cursorrules/error-handling.md +190 -0
- package/templates/rust-expert/.cursorrules/overview.md +142 -0
- package/templates/rust-expert/.cursorrules/ownership-and-borrowing.md +204 -0
- package/templates/rust-expert/.cursorrules/performance-and-unsafe.md +256 -0
- package/templates/rust-expert/.cursorrules/testing.md +300 -0
- package/templates/rust-expert/.cursorrules/traits-and-generics.md +236 -0
- package/templates/rust-expert/CLAUDE.md +283 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
# Rust Ecosystem and Tooling
|
|
2
|
+
|
|
3
|
+
The Rust ecosystem is mature and opinionated. The toolchain is best-in-class. Know the tools and the community-standard crates.
|
|
4
|
+
|
|
5
|
+
## Cargo
|
|
6
|
+
|
|
7
|
+
### Cargo.toml Best Practices
|
|
8
|
+
|
|
9
|
+
```toml
|
|
10
|
+
[package]
|
|
11
|
+
name = "my-crate"
|
|
12
|
+
version = "0.1.0"
|
|
13
|
+
edition = "2021"
|
|
14
|
+
rust-version = "1.75" # Minimum supported Rust version (MSRV)
|
|
15
|
+
description = "A brief description"
|
|
16
|
+
license = "MIT OR Apache-2.0"
|
|
17
|
+
repository = "https://github.com/user/repo"
|
|
18
|
+
|
|
19
|
+
[dependencies]
|
|
20
|
+
serde = { version = "1", features = ["derive"] }
|
|
21
|
+
tokio = { version = "1", features = ["full"] }
|
|
22
|
+
|
|
23
|
+
[dev-dependencies]
|
|
24
|
+
criterion = { version = "0.5", features = ["html_reports"] }
|
|
25
|
+
proptest = "1"
|
|
26
|
+
tempfile = "3"
|
|
27
|
+
|
|
28
|
+
[profile.release]
|
|
29
|
+
lto = true # Link-time optimization
|
|
30
|
+
codegen-units = 1 # Slower compile, better optimization
|
|
31
|
+
strip = true # Strip debug symbols from binary
|
|
32
|
+
|
|
33
|
+
[profile.dev]
|
|
34
|
+
opt-level = 0 # Fast compile for development
|
|
35
|
+
|
|
36
|
+
# Feature flags must be additive
|
|
37
|
+
[features]
|
|
38
|
+
default = []
|
|
39
|
+
metrics = ["prometheus"]
|
|
40
|
+
tracing = ["tracing-subscriber", "tracing-opentelemetry"]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Workspace Management
|
|
44
|
+
|
|
45
|
+
```toml
|
|
46
|
+
# Root Cargo.toml
|
|
47
|
+
[workspace]
|
|
48
|
+
members = ["crates/*"]
|
|
49
|
+
resolver = "2"
|
|
50
|
+
|
|
51
|
+
# Share dependencies across workspace
|
|
52
|
+
[workspace.dependencies]
|
|
53
|
+
serde = { version = "1", features = ["derive"] }
|
|
54
|
+
tokio = { version = "1", features = ["full"] }
|
|
55
|
+
anyhow = "1"
|
|
56
|
+
|
|
57
|
+
# In member Cargo.toml:
|
|
58
|
+
[dependencies]
|
|
59
|
+
serde = { workspace = true }
|
|
60
|
+
tokio = { workspace = true }
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Essential Cargo Commands
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
cargo build --release # Optimized build
|
|
67
|
+
cargo test -- --nocapture # Show println! output in tests
|
|
68
|
+
cargo doc --open # Build and open docs
|
|
69
|
+
cargo tree # Dependency tree
|
|
70
|
+
cargo tree -d # Show duplicate dependencies
|
|
71
|
+
cargo update # Update Cargo.lock to latest compatible
|
|
72
|
+
cargo audit # Check for known vulnerabilities
|
|
73
|
+
cargo deny check # License and advisory checks
|
|
74
|
+
cargo expand # Show macro expansion
|
|
75
|
+
cargo outdated # Show outdated dependencies
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## Linting and Formatting
|
|
79
|
+
|
|
80
|
+
### Clippy Configuration
|
|
81
|
+
|
|
82
|
+
```toml
|
|
83
|
+
# clippy.toml or .clippy.toml
|
|
84
|
+
cognitive-complexity-threshold = 25
|
|
85
|
+
too-many-arguments-threshold = 7
|
|
86
|
+
type-complexity-threshold = 250
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
```rust
|
|
90
|
+
// In lib.rs or main.rs — project-wide lint configuration
|
|
91
|
+
#![warn(clippy::all, clippy::pedantic)]
|
|
92
|
+
#![allow(clippy::module_name_repetitions)] // Allow if your API style needs it
|
|
93
|
+
#![deny(clippy::unwrap_used)] // No unwrap in library code
|
|
94
|
+
#![deny(unsafe_code)] // Deny unsafe unless explicitly needed
|
|
95
|
+
|
|
96
|
+
// Per-module overrides when justified
|
|
97
|
+
#[allow(clippy::cast_possible_truncation)]
|
|
98
|
+
// Justification: value is guaranteed < 256 by the protocol spec
|
|
99
|
+
fn protocol_byte(value: u32) -> u8 {
|
|
100
|
+
value as u8
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### rustfmt
|
|
105
|
+
|
|
106
|
+
```toml
|
|
107
|
+
# rustfmt.toml
|
|
108
|
+
edition = "2021"
|
|
109
|
+
max_width = 100
|
|
110
|
+
use_field_init_shorthand = true
|
|
111
|
+
use_try_shorthand = true
|
|
112
|
+
imports_granularity = "Crate"
|
|
113
|
+
group_imports = "StdExternalCrate"
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Essential Crates
|
|
117
|
+
|
|
118
|
+
### Serialization
|
|
119
|
+
- **serde** + **serde_json** — the serialization framework, period
|
|
120
|
+
- **toml** — config files
|
|
121
|
+
- **bincode** — compact binary format
|
|
122
|
+
|
|
123
|
+
### Async Runtime
|
|
124
|
+
- **tokio** — the dominant async runtime (multi-threaded, work-stealing)
|
|
125
|
+
- **async-std** — alternative, closer to std API
|
|
126
|
+
|
|
127
|
+
### Web
|
|
128
|
+
- **axum** — ergonomic web framework built on Tower and Hyper
|
|
129
|
+
- **reqwest** — HTTP client
|
|
130
|
+
- **tonic** — gRPC
|
|
131
|
+
|
|
132
|
+
### Database
|
|
133
|
+
- **sqlx** — compile-time checked SQL queries
|
|
134
|
+
- **diesel** — ORM with type-safe query builder
|
|
135
|
+
- **sea-orm** — async ORM built on sqlx
|
|
136
|
+
|
|
137
|
+
### Error Handling
|
|
138
|
+
- **thiserror** — derive Error for library error types
|
|
139
|
+
- **anyhow** — flexible error handling for applications
|
|
140
|
+
|
|
141
|
+
### CLI
|
|
142
|
+
- **clap** — argument parsing (derive or builder API)
|
|
143
|
+
|
|
144
|
+
### Observability
|
|
145
|
+
- **tracing** — structured, async-aware instrumentation
|
|
146
|
+
- **metrics** — application metrics
|
|
147
|
+
- **opentelemetry** — distributed tracing
|
|
148
|
+
|
|
149
|
+
### Testing
|
|
150
|
+
- **proptest** — property-based testing
|
|
151
|
+
- **criterion** — statistical benchmarking
|
|
152
|
+
- **mockall** — mocking framework
|
|
153
|
+
- **wiremock** — HTTP mocking for integration tests
|
|
154
|
+
- **tempfile** — temporary files and directories for tests
|
|
155
|
+
- **insta** — snapshot testing
|
|
156
|
+
|
|
157
|
+
## CI/CD
|
|
158
|
+
|
|
159
|
+
### GitHub Actions
|
|
160
|
+
|
|
161
|
+
```yaml
|
|
162
|
+
name: CI
|
|
163
|
+
|
|
164
|
+
on: [push, pull_request]
|
|
165
|
+
|
|
166
|
+
env:
|
|
167
|
+
CARGO_TERM_COLOR: always
|
|
168
|
+
RUSTFLAGS: "-Dwarnings"
|
|
169
|
+
|
|
170
|
+
jobs:
|
|
171
|
+
check:
|
|
172
|
+
runs-on: ubuntu-latest
|
|
173
|
+
steps:
|
|
174
|
+
- uses: actions/checkout@v4
|
|
175
|
+
- uses: dtolnay/rust-toolchain@stable
|
|
176
|
+
with:
|
|
177
|
+
components: rustfmt, clippy
|
|
178
|
+
- uses: Swatinem/rust-cache@v2
|
|
179
|
+
|
|
180
|
+
- run: cargo fmt -- --check
|
|
181
|
+
- run: cargo clippy --all-targets --all-features
|
|
182
|
+
- run: cargo test --all-features
|
|
183
|
+
- run: cargo doc --no-deps --all-features
|
|
184
|
+
|
|
185
|
+
# Miri for crates with unsafe code
|
|
186
|
+
miri:
|
|
187
|
+
runs-on: ubuntu-latest
|
|
188
|
+
steps:
|
|
189
|
+
- uses: actions/checkout@v4
|
|
190
|
+
- uses: dtolnay/rust-toolchain@nightly
|
|
191
|
+
with:
|
|
192
|
+
components: miri
|
|
193
|
+
- run: cargo +nightly miri test
|
|
194
|
+
|
|
195
|
+
# Security audit
|
|
196
|
+
audit:
|
|
197
|
+
runs-on: ubuntu-latest
|
|
198
|
+
steps:
|
|
199
|
+
- uses: actions/checkout@v4
|
|
200
|
+
- uses: rustsec/audit-check@v2
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Makefile
|
|
204
|
+
|
|
205
|
+
```makefile
|
|
206
|
+
.PHONY: check test lint fmt doc
|
|
207
|
+
|
|
208
|
+
check: fmt lint test
|
|
209
|
+
|
|
210
|
+
fmt:
|
|
211
|
+
cargo fmt -- --check
|
|
212
|
+
|
|
213
|
+
lint:
|
|
214
|
+
cargo clippy --all-targets --all-features -- -D warnings
|
|
215
|
+
|
|
216
|
+
test:
|
|
217
|
+
cargo test --all-features
|
|
218
|
+
|
|
219
|
+
doc:
|
|
220
|
+
cargo doc --no-deps --all-features --open
|
|
221
|
+
|
|
222
|
+
audit:
|
|
223
|
+
cargo audit
|
|
224
|
+
cargo deny check
|
|
225
|
+
|
|
226
|
+
bench:
|
|
227
|
+
cargo bench
|
|
228
|
+
|
|
229
|
+
coverage:
|
|
230
|
+
cargo tarpaulin --out Html --output-dir target/coverage
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Documentation
|
|
234
|
+
|
|
235
|
+
```rust
|
|
236
|
+
//! # My Crate
|
|
237
|
+
//!
|
|
238
|
+
//! `my_crate` provides utilities for processing data efficiently.
|
|
239
|
+
//!
|
|
240
|
+
//! ## Quick Start
|
|
241
|
+
//!
|
|
242
|
+
//! ```rust
|
|
243
|
+
//! use my_crate::process;
|
|
244
|
+
//!
|
|
245
|
+
//! let result = process("input data")?;
|
|
246
|
+
//! println!("{result}");
|
|
247
|
+
//! # Ok::<(), my_crate::Error>(())
|
|
248
|
+
//! ```
|
|
249
|
+
|
|
250
|
+
/// Processes the input data and returns a formatted result.
|
|
251
|
+
///
|
|
252
|
+
/// # Arguments
|
|
253
|
+
///
|
|
254
|
+
/// * `input` - A string slice containing the raw data
|
|
255
|
+
///
|
|
256
|
+
/// # Errors
|
|
257
|
+
///
|
|
258
|
+
/// Returns [`Error::InvalidInput`] if the input is malformed.
|
|
259
|
+
///
|
|
260
|
+
/// # Examples
|
|
261
|
+
///
|
|
262
|
+
/// ```
|
|
263
|
+
/// use my_crate::process;
|
|
264
|
+
///
|
|
265
|
+
/// let output = process("valid input").unwrap();
|
|
266
|
+
/// assert_eq!(output, "processed: valid input");
|
|
267
|
+
/// ```
|
|
268
|
+
pub fn process(input: &str) -> Result<String, Error> {
|
|
269
|
+
// ...
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Document panic conditions
|
|
273
|
+
/// # Panics
|
|
274
|
+
///
|
|
275
|
+
/// Panics if `divisor` is zero.
|
|
276
|
+
pub fn divide(a: i64, divisor: i64) -> i64 {
|
|
277
|
+
assert_ne!(divisor, 0, "divisor must be non-zero");
|
|
278
|
+
a / divisor
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Document safety requirements for unsafe functions
|
|
282
|
+
/// # Safety
|
|
283
|
+
///
|
|
284
|
+
/// - `ptr` must be valid for reads of `len` bytes
|
|
285
|
+
/// - `ptr` must be properly aligned for `T`
|
|
286
|
+
/// - The memory referenced must not be mutated during the lifetime of the returned slice
|
|
287
|
+
pub unsafe fn from_raw_parts<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
|
|
288
|
+
std::slice::from_raw_parts(ptr, len)
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
## Dependency Hygiene
|
|
293
|
+
|
|
294
|
+
- **Audit regularly**: `cargo audit` for security advisories
|
|
295
|
+
- **Check licenses**: `cargo deny check licenses`
|
|
296
|
+
- **Minimize dependencies**: Every dependency is attack surface and compile time
|
|
297
|
+
- **Pin workspace deps**: Use `[workspace.dependencies]` for consistency
|
|
298
|
+
- **Review before adding**: Check maintenance status, download counts, and bus factor
|
|
299
|
+
- **Feature flags**: Only enable features you actually use
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
# Rust Error Handling
|
|
2
|
+
|
|
3
|
+
Rust has no exceptions. `Result<T, E>` and `Option<T>` are the error handling primitives. The `?` operator propagates errors ergonomically, but every error path is still an explicit decision.
|
|
4
|
+
|
|
5
|
+
## Custom Error Types
|
|
6
|
+
|
|
7
|
+
### Using thiserror (Libraries)
|
|
8
|
+
|
|
9
|
+
```rust
|
|
10
|
+
use thiserror::Error;
|
|
11
|
+
|
|
12
|
+
#[derive(Debug, Error)]
|
|
13
|
+
pub enum AppError {
|
|
14
|
+
#[error("configuration error: {message}")]
|
|
15
|
+
Config { message: String },
|
|
16
|
+
|
|
17
|
+
#[error("database query failed")]
|
|
18
|
+
Database(#[from] sqlx::Error),
|
|
19
|
+
|
|
20
|
+
#[error("request to {url} failed")]
|
|
21
|
+
Http {
|
|
22
|
+
url: String,
|
|
23
|
+
#[source]
|
|
24
|
+
source: reqwest::Error,
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
#[error("record not found: {entity} with id {id}")]
|
|
28
|
+
NotFound { entity: &'static str, id: String },
|
|
29
|
+
|
|
30
|
+
#[error("validation failed: {0}")]
|
|
31
|
+
Validation(String),
|
|
32
|
+
}
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Using anyhow (Applications)
|
|
36
|
+
|
|
37
|
+
```rust
|
|
38
|
+
use anyhow::{Context, Result, bail, ensure};
|
|
39
|
+
|
|
40
|
+
fn load_config(path: &Path) -> Result<Config> {
|
|
41
|
+
let contents = fs::read_to_string(path)
|
|
42
|
+
.with_context(|| format!("reading config from {}", path.display()))?;
|
|
43
|
+
|
|
44
|
+
let config: Config = toml::from_str(&contents)
|
|
45
|
+
.context("parsing config TOML")?;
|
|
46
|
+
|
|
47
|
+
ensure!(!config.database_url.is_empty(), "database_url must not be empty");
|
|
48
|
+
|
|
49
|
+
if config.port == 0 {
|
|
50
|
+
bail!("port must be non-zero");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
Ok(config)
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### When to Use Which
|
|
58
|
+
|
|
59
|
+
- **thiserror** — libraries, reusable crates, when callers need to match on error variants
|
|
60
|
+
- **anyhow** — applications, binaries, when you just need to propagate context up to main
|
|
61
|
+
- **Manual impl** — when you need full control or minimal dependencies
|
|
62
|
+
|
|
63
|
+
### Manual Error Implementation
|
|
64
|
+
|
|
65
|
+
```rust
|
|
66
|
+
#[derive(Debug)]
|
|
67
|
+
pub enum StorageError {
|
|
68
|
+
Io(std::io::Error),
|
|
69
|
+
Serialization(serde_json::Error),
|
|
70
|
+
NotFound { key: String },
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
impl fmt::Display for StorageError {
|
|
74
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
75
|
+
match self {
|
|
76
|
+
Self::Io(e) => write!(f, "storage I/O error: {e}"),
|
|
77
|
+
Self::Serialization(e) => write!(f, "serialization error: {e}"),
|
|
78
|
+
Self::NotFound { key } => write!(f, "key not found: {key}"),
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
impl std::error::Error for StorageError {
|
|
84
|
+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
85
|
+
match self {
|
|
86
|
+
Self::Io(e) => Some(e),
|
|
87
|
+
Self::Serialization(e) => Some(e),
|
|
88
|
+
Self::NotFound { .. } => None,
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
impl From<std::io::Error> for StorageError {
|
|
94
|
+
fn from(e: std::io::Error) -> Self {
|
|
95
|
+
Self::Io(e)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## The ? Operator
|
|
101
|
+
|
|
102
|
+
```rust
|
|
103
|
+
// ? is sugar for early return on Err/None
|
|
104
|
+
// It calls From::from() on the error, enabling automatic conversion
|
|
105
|
+
|
|
106
|
+
fn process(path: &Path) -> Result<Data, AppError> {
|
|
107
|
+
let raw = fs::read(path)?; // io::Error -> AppError via From
|
|
108
|
+
let parsed = serde_json::from_slice(&raw)?; // serde::Error -> AppError via From
|
|
109
|
+
Ok(parsed)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ? works with Option too
|
|
113
|
+
fn first_word(s: &str) -> Option<&str> {
|
|
114
|
+
let end = s.find(' ')?; // Returns None if no space
|
|
115
|
+
Some(&s[..end])
|
|
116
|
+
}
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## Option Patterns
|
|
120
|
+
|
|
121
|
+
```rust
|
|
122
|
+
// Prefer combinators over match when they're clearer
|
|
123
|
+
let name = user.name.as_deref().unwrap_or("anonymous");
|
|
124
|
+
|
|
125
|
+
let parsed = input
|
|
126
|
+
.strip_prefix("v")
|
|
127
|
+
.and_then(|s| s.parse::<u32>().ok());
|
|
128
|
+
|
|
129
|
+
// Use match when you need complex logic per variant
|
|
130
|
+
match config.log_level {
|
|
131
|
+
Some(level) => logger.set_level(level),
|
|
132
|
+
None => logger.set_level(Level::Info),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Use if let for single-variant checks
|
|
136
|
+
if let Some(token) = headers.get("Authorization") {
|
|
137
|
+
authenticate(token)?;
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Panic Policy
|
|
142
|
+
|
|
143
|
+
```rust
|
|
144
|
+
// Panics are for bugs, not for expected error conditions.
|
|
145
|
+
|
|
146
|
+
// Acceptable: debug assertions
|
|
147
|
+
debug_assert!(index < len, "index {index} out of bounds for length {len}");
|
|
148
|
+
|
|
149
|
+
// Acceptable: unreachable code paths that truly can't happen
|
|
150
|
+
match state {
|
|
151
|
+
State::Ready => process(),
|
|
152
|
+
State::Done => return,
|
|
153
|
+
// If we've exhaustively handled all variants and the compiler
|
|
154
|
+
// still wants a branch, use unreachable!() — but only when
|
|
155
|
+
// you can prove it's unreachable.
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Acceptable: unwrap() in tests
|
|
159
|
+
#[cfg(test)]
|
|
160
|
+
fn setup() -> TestDb {
|
|
161
|
+
TestDb::new().unwrap() // Fine in tests
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Never acceptable: unwrap() in library code without a VERY good reason
|
|
165
|
+
// Never acceptable: panic!() for user input validation
|
|
166
|
+
// Never acceptable: todo!() in shipped code
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Anti-Patterns
|
|
170
|
+
|
|
171
|
+
```rust
|
|
172
|
+
// Never: Silencing errors
|
|
173
|
+
let _ = write!(f, "data"); // The write might fail — handle it or explain why not
|
|
174
|
+
|
|
175
|
+
// Never: unwrap() with no context
|
|
176
|
+
let config = load_config().unwrap(); // What failed? Where? Why?
|
|
177
|
+
// At minimum:
|
|
178
|
+
let config = load_config().expect("loading config from default path");
|
|
179
|
+
|
|
180
|
+
// Never: Stringly-typed errors
|
|
181
|
+
fn process() -> Result<(), String> { // Don't use String as an error type
|
|
182
|
+
Err("something went wrong".into())
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Never: Catching panics as error handling
|
|
186
|
+
let result = std::panic::catch_unwind(|| dangerous_code()); // This is not error handling
|
|
187
|
+
|
|
188
|
+
// Never: .ok() to discard error information without justification
|
|
189
|
+
let maybe = fallible_operation().ok(); // Why is the error not important?
|
|
190
|
+
```
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# Rust Expert
|
|
2
|
+
|
|
3
|
+
Guidelines for principal-level Rust engineering. Ownership, zero-cost abstractions, and fearless concurrency — wielded with precision.
|
|
4
|
+
|
|
5
|
+
## Scope
|
|
6
|
+
|
|
7
|
+
This ruleset applies to:
|
|
8
|
+
- Systems programming and embedded targets
|
|
9
|
+
- Web services and async runtimes (Tokio, async-std)
|
|
10
|
+
- CLI tools and developer utilities
|
|
11
|
+
- Libraries and crates published to crates.io
|
|
12
|
+
- WebAssembly targets
|
|
13
|
+
- FFI and interop with C/C++
|
|
14
|
+
- Performance-critical applications
|
|
15
|
+
|
|
16
|
+
## Core Philosophy
|
|
17
|
+
|
|
18
|
+
Rust gives you power without sacrificing safety. The compiler is your closest collaborator — work with it, not against it.
|
|
19
|
+
|
|
20
|
+
- **If it compiles, it's probably correct.** The borrow checker isn't an obstacle — it's catching bugs that would have been CVEs in another language. Trust it.
|
|
21
|
+
- **Zero-cost abstractions are the point.** You should never have to choose between expressiveness and performance. If the abstraction has runtime cost, question whether it's the right abstraction.
|
|
22
|
+
- **Make illegal states unrepresentable.** Use the type system to encode invariants. If a state shouldn't exist, make it impossible to construct.
|
|
23
|
+
- **Explicit over implicit.** Lifetimes, ownership, error handling — Rust surfaces what other languages hide. This is a feature, not a flaw.
|
|
24
|
+
- **Unsafe is a scalpel, not a sledgehammer.** Every `unsafe` block is a proof obligation. If you can't explain exactly why it's sound, don't write it.
|
|
25
|
+
- **If you don't know, say so.** The Rust ecosystem is vast and evolving. Admitting uncertainty is better than guessing wrong about soundness.
|
|
26
|
+
|
|
27
|
+
## Key Principles
|
|
28
|
+
|
|
29
|
+
### 1. Ownership Is the Architecture
|
|
30
|
+
|
|
31
|
+
Ownership isn't just memory management — it's your design tool:
|
|
32
|
+
- Who owns this data? Who borrows it? For how long?
|
|
33
|
+
- These questions force you to design clear APIs with explicit lifetimes and responsibilities
|
|
34
|
+
- If you're fighting the borrow checker, the design likely needs rethinking — not an `unsafe` escape hatch
|
|
35
|
+
|
|
36
|
+
### 2. Types Encode Invariants
|
|
37
|
+
|
|
38
|
+
```rust
|
|
39
|
+
// Make illegal states unrepresentable
|
|
40
|
+
// Bad: bool flags that can be inconsistent
|
|
41
|
+
struct Connection {
|
|
42
|
+
is_connected: bool,
|
|
43
|
+
is_authenticated: bool, // Can this be true if is_connected is false?
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Good: State machine as an enum
|
|
47
|
+
enum Connection {
|
|
48
|
+
Disconnected,
|
|
49
|
+
Connected(TcpStream),
|
|
50
|
+
Authenticated { stream: TcpStream, token: AuthToken },
|
|
51
|
+
}
|
|
52
|
+
// It's impossible to be Authenticated without a stream
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### 3. Error Handling Is Explicit
|
|
56
|
+
|
|
57
|
+
```rust
|
|
58
|
+
// Rust has no exceptions. Result and Option are the tools.
|
|
59
|
+
// The ? operator is syntactic sugar, not an excuse to ignore errors.
|
|
60
|
+
|
|
61
|
+
fn load_config(path: &Path) -> Result<Config, ConfigError> {
|
|
62
|
+
let contents = fs::read_to_string(path)
|
|
63
|
+
.map_err(|e| ConfigError::ReadFailed { path: path.to_owned(), source: e })?;
|
|
64
|
+
let config: Config = toml::from_str(&contents)
|
|
65
|
+
.map_err(|e| ConfigError::ParseFailed { source: e })?;
|
|
66
|
+
config.validate()?;
|
|
67
|
+
Ok(config)
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### 4. Clippy Is Non-Negotiable
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
# In CI, always:
|
|
75
|
+
cargo clippy -- -D warnings
|
|
76
|
+
# Clippy catches real bugs, not just style nits. Treat warnings as errors.
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Project Structure
|
|
80
|
+
|
|
81
|
+
```
|
|
82
|
+
my-project/
|
|
83
|
+
├── Cargo.toml # Workspace or package manifest
|
|
84
|
+
├── Cargo.lock # Committed for binaries, not for libraries
|
|
85
|
+
├── src/
|
|
86
|
+
│ ├── main.rs # Binary entry point (or lib.rs for libraries)
|
|
87
|
+
│ ├── lib.rs # Library root — public API surface
|
|
88
|
+
│ ├── config.rs # Configuration loading and validation
|
|
89
|
+
│ ├── error.rs # Error types for this crate
|
|
90
|
+
│ └── domain/ # Core business logic modules
|
|
91
|
+
│ ├── mod.rs
|
|
92
|
+
│ └── ...
|
|
93
|
+
├── tests/ # Integration tests (separate compilation unit)
|
|
94
|
+
│ └── integration_test.rs
|
|
95
|
+
├── benches/ # Benchmarks
|
|
96
|
+
│ └── benchmark.rs
|
|
97
|
+
├── examples/ # Runnable examples
|
|
98
|
+
│ └── basic_usage.rs
|
|
99
|
+
└── build.rs # Build script (if needed)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### Workspace Layout (Multi-Crate)
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
workspace/
|
|
106
|
+
├── Cargo.toml # [workspace] members
|
|
107
|
+
├── crates/
|
|
108
|
+
│ ├── core/ # Domain logic — no IO, no async
|
|
109
|
+
│ │ ├── Cargo.toml
|
|
110
|
+
│ │ └── src/lib.rs
|
|
111
|
+
│ ├── api/ # HTTP handlers, routes
|
|
112
|
+
│ │ ├── Cargo.toml
|
|
113
|
+
│ │ └── src/lib.rs
|
|
114
|
+
│ ├── db/ # Database access
|
|
115
|
+
│ │ ├── Cargo.toml
|
|
116
|
+
│ │ └── src/lib.rs
|
|
117
|
+
│ └── cli/ # Binary entry point
|
|
118
|
+
│ ├── Cargo.toml
|
|
119
|
+
│ └── src/main.rs
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Layout Rules
|
|
123
|
+
|
|
124
|
+
- `lib.rs` defines the public API — only `pub` items here are your contract
|
|
125
|
+
- `mod.rs` or file-per-module — be consistent within a project
|
|
126
|
+
- Integration tests in `tests/` are separate compilation units — they test your public API
|
|
127
|
+
- `Cargo.lock` is committed for binaries and applications, not for libraries
|
|
128
|
+
- Feature flags in `Cargo.toml` must be additive — enabling a feature must not break existing code
|
|
129
|
+
|
|
130
|
+
## Definition of Done
|
|
131
|
+
|
|
132
|
+
A Rust feature is complete when:
|
|
133
|
+
- [ ] `cargo build` compiles with zero warnings
|
|
134
|
+
- [ ] `cargo clippy -- -D warnings` passes
|
|
135
|
+
- [ ] `cargo test` passes (including doc tests)
|
|
136
|
+
- [ ] `cargo fmt -- --check` passes
|
|
137
|
+
- [ ] No `unwrap()` or `expect()` in library code without justification
|
|
138
|
+
- [ ] All `unsafe` blocks have `// SAFETY:` comments explaining the invariants
|
|
139
|
+
- [ ] Error types are meaningful and implement `std::error::Error`
|
|
140
|
+
- [ ] Public API has doc comments with examples
|
|
141
|
+
- [ ] No unnecessary allocations in hot paths
|
|
142
|
+
- [ ] `cargo deny check` passes (license and advisory audit)
|