@canonical/code-standards 0.1.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/.mcp.json +8 -0
- package/README.md +297 -0
- package/data/code.ttl +437 -0
- package/data/css.ttl +265 -0
- package/data/icons.ttl +359 -0
- package/data/packaging.ttl +464 -0
- package/data/react.ttl +752 -0
- package/data/rust.ttl +1806 -0
- package/data/storybook.ttl +403 -0
- package/data/styling.ttl +165 -0
- package/data/tsdoc.ttl +216 -0
- package/data/turtle.ttl +179 -0
- package/definitions/CodeStandard.ttl +80 -0
- package/docs/code.md +720 -0
- package/docs/css.md +275 -0
- package/docs/icons.md +367 -0
- package/docs/index.md +15 -0
- package/docs/react.md +766 -0
- package/docs/rust.md +1784 -0
- package/docs/storybook.md +413 -0
- package/docs/styling.md +163 -0
- package/docs/tsdoc.md +213 -0
- package/docs/turtle.md +179 -0
- package/package.json +9 -0
- package/skills/add-standard/SKILL.md +288 -0
- package/src/scripts/generate-docs.ts +131 -0
- package/src/scripts/index.ts +19 -0
package/docs/rust.md
ADDED
|
@@ -0,0 +1,1784 @@
|
|
|
1
|
+
# Rust Standards
|
|
2
|
+
|
|
3
|
+
Standards for rust development.
|
|
4
|
+
|
|
5
|
+
## rust/composition/result-chaining
|
|
6
|
+
|
|
7
|
+
Use monadic Result chaining with the ? operator and combinators like and_then, map, and or_else for clean, composable error handling. This is the Rust equivalent of Haskell's do-notation for the Either monad.
|
|
8
|
+
|
|
9
|
+
**Ratings:**
|
|
10
|
+
- Impact: A (cleaner control flow, eliminates nested matches)
|
|
11
|
+
- Feasibility: S (already natural in Rust)
|
|
12
|
+
- Idiomaticity: S (idiomatic Rust; ? operator is standard)
|
|
13
|
+
- FP Purity: S (direct monadic composition / Kleisli arrows)
|
|
14
|
+
|
|
15
|
+
### Do
|
|
16
|
+
|
|
17
|
+
(Do) Use the ? operator for clean Result propagation.
|
|
18
|
+
```rust
|
|
19
|
+
fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
|
|
20
|
+
let project_root = find_project_root()
|
|
21
|
+
.ok_or(GraphError::NoProjectRoot)?;
|
|
22
|
+
|
|
23
|
+
let manifest = load_manifest(&project_root)
|
|
24
|
+
.map_err(|e| GraphError::InvalidManifest {
|
|
25
|
+
path: project_root.clone(),
|
|
26
|
+
message: e,
|
|
27
|
+
})?;
|
|
28
|
+
|
|
29
|
+
let (store, package_graphs) = match manifest.manifest_type() {
|
|
30
|
+
ManifestType::Workspace => load_workspace_graph(&project_root)?,
|
|
31
|
+
ManifestType::Package => {
|
|
32
|
+
let (store, graph) = load_package_graph(&project_root)?;
|
|
33
|
+
(store, vec![graph])
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
Ok((store, package_graphs))
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
(Do) Use and_then for dependent computations.
|
|
42
|
+
```rust
|
|
43
|
+
fn process_user_input(input: &str) -> Result<Output, ProcessError> {
|
|
44
|
+
parse_input(input)
|
|
45
|
+
.and_then(|parsed| validate(parsed))
|
|
46
|
+
.and_then(|valid| transform(valid))
|
|
47
|
+
.and_then(|transformed| save(transformed))
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
(Do) Use map for infallible transformations within Result.
|
|
52
|
+
```rust
|
|
53
|
+
fn get_package_names(path: &Path) -> Result<Vec<String>, IoError> {
|
|
54
|
+
fs::read_dir(path)?
|
|
55
|
+
.filter_map(|entry| entry.ok())
|
|
56
|
+
.map(|entry| entry.file_name().to_string_lossy().to_string())
|
|
57
|
+
.collect::<Vec<_>>()
|
|
58
|
+
.pipe(Ok) // or just wrap in Ok()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Or more idiomatically:
|
|
62
|
+
fn get_config_value(key: &str) -> Result<String, ConfigError> {
|
|
63
|
+
load_config()?
|
|
64
|
+
.get(key)
|
|
65
|
+
.map(|v| v.to_string())
|
|
66
|
+
.ok_or(ConfigError::MissingKey(key.to_string()))
|
|
67
|
+
}
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
(Do) Use or_else for fallback computations.
|
|
71
|
+
```rust
|
|
72
|
+
fn find_config() -> Result<Config, ConfigError> {
|
|
73
|
+
load_config_from_env()
|
|
74
|
+
.or_else(|_| load_config_from_file())
|
|
75
|
+
.or_else(|_| Ok(Config::default()))
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
(Do) Combine multiple Results with the ? operator in sequence.
|
|
80
|
+
```rust
|
|
81
|
+
fn setup_application() -> Result<App, SetupError> {
|
|
82
|
+
let config = load_config()?;
|
|
83
|
+
let db = connect_database(&config.db_url)?;
|
|
84
|
+
let cache = initialize_cache(&config.cache)?;
|
|
85
|
+
let logger = setup_logging(&config.log)?;
|
|
86
|
+
|
|
87
|
+
Ok(App { config, db, cache, logger })
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
### Don't
|
|
92
|
+
|
|
93
|
+
(Don't) Use nested match expressions for Result handling.
|
|
94
|
+
```rust
|
|
95
|
+
// Bad: Deeply nested, hard to follow
|
|
96
|
+
fn process(input: &str) -> Result<Output, Error> {
|
|
97
|
+
match parse(input) {
|
|
98
|
+
Ok(parsed) => {
|
|
99
|
+
match validate(parsed) {
|
|
100
|
+
Ok(valid) => {
|
|
101
|
+
match transform(valid) {
|
|
102
|
+
Ok(output) => Ok(output),
|
|
103
|
+
Err(e) => Err(e.into()),
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
Err(e) => Err(e.into()),
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
Err(e) => Err(e.into()),
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
(Don't) Use unwrap() or expect() to bypass error handling.
|
|
115
|
+
```rust
|
|
116
|
+
// Bad: Panics on error
|
|
117
|
+
fn load_data() -> Data {
|
|
118
|
+
let content = fs::read_to_string("data.json").unwrap();
|
|
119
|
+
serde_json::from_str(&content).unwrap()
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
(Don't) Ignore errors silently.
|
|
124
|
+
```rust
|
|
125
|
+
// Bad: Errors are silently dropped
|
|
126
|
+
fn try_save(data: &Data) {
|
|
127
|
+
let _ = fs::write("data.json", serde_json::to_string(data).unwrap_or_default());
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
(Don't) Convert all errors to strings early in the chain.
|
|
132
|
+
```rust
|
|
133
|
+
// Bad: Loses error type information
|
|
134
|
+
fn process() -> Result<(), String> {
|
|
135
|
+
let x = step1().map_err(|e| e.to_string())?;
|
|
136
|
+
let y = step2(x).map_err(|e| e.to_string())?; // Can't distinguish errors
|
|
137
|
+
Ok(())
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## rust/errors/context-enrichment
|
|
144
|
+
|
|
145
|
+
Always enrich errors with contextual information such as file paths, operation names, and relevant state. This dramatically improves debugging experience and follows the principle of errors as values.
|
|
146
|
+
|
|
147
|
+
**Ratings:**
|
|
148
|
+
- Impact: S (dramatically improves debugging experience)
|
|
149
|
+
- Feasibility: A (thiserror + map_err pattern already established)
|
|
150
|
+
- Idiomaticity: S (Rust community best practice)
|
|
151
|
+
- FP Purity: B (error as value; monadic error propagation)
|
|
152
|
+
|
|
153
|
+
### Do
|
|
154
|
+
|
|
155
|
+
(Do) Use thiserror to create structured, contextual error types.
|
|
156
|
+
```rust
|
|
157
|
+
use thiserror::Error;
|
|
158
|
+
use std::path::PathBuf;
|
|
159
|
+
|
|
160
|
+
#[derive(Error, Debug)]
|
|
161
|
+
pub enum GraphError {
|
|
162
|
+
#[error("Failed to read file {path}: {source}")]
|
|
163
|
+
FileRead {
|
|
164
|
+
path: PathBuf,
|
|
165
|
+
#[source]
|
|
166
|
+
source: std::io::Error,
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
#[error("Failed to parse Turtle file {path}: {message}")]
|
|
170
|
+
TurtleParse {
|
|
171
|
+
path: PathBuf,
|
|
172
|
+
message: String,
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
#[error("SPARQL query error: {0}")]
|
|
176
|
+
SparqlQuery(String),
|
|
177
|
+
|
|
178
|
+
#[error("Invalid manifest at {path}: {message}")]
|
|
179
|
+
InvalidManifest {
|
|
180
|
+
path: PathBuf,
|
|
181
|
+
message: String,
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
(Do) Use map_err to add context when propagating errors.
|
|
187
|
+
```rust
|
|
188
|
+
fn load_package(path: &Path) -> Result<Package, GraphError> {
|
|
189
|
+
let content = fs::read_to_string(path)
|
|
190
|
+
.map_err(|e| GraphError::FileRead {
|
|
191
|
+
path: path.to_path_buf(),
|
|
192
|
+
source: e,
|
|
193
|
+
})?;
|
|
194
|
+
|
|
195
|
+
let manifest: Manifest = toml::from_str(&content)
|
|
196
|
+
.map_err(|e| GraphError::InvalidManifest {
|
|
197
|
+
path: path.to_path_buf(),
|
|
198
|
+
message: e.to_string(),
|
|
199
|
+
})?;
|
|
200
|
+
|
|
201
|
+
Ok(Package::from_manifest(manifest))
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
(Do) Chain errors to preserve the full error trail.
|
|
206
|
+
```rust
|
|
207
|
+
#[derive(Error, Debug)]
|
|
208
|
+
pub enum AppError {
|
|
209
|
+
#[error("Failed to load configuration")]
|
|
210
|
+
Config(#[from] ConfigError),
|
|
211
|
+
|
|
212
|
+
#[error("Database operation failed")]
|
|
213
|
+
Database(#[from] DatabaseError),
|
|
214
|
+
|
|
215
|
+
#[error("Package loading failed for '{package}'")]
|
|
216
|
+
PackageLoad {
|
|
217
|
+
package: String,
|
|
218
|
+
#[source]
|
|
219
|
+
source: GraphError,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
(Do) Include actionable information in error messages.
|
|
225
|
+
```rust
|
|
226
|
+
#[derive(Error, Debug)]
|
|
227
|
+
pub enum ValidationError {
|
|
228
|
+
#[error("Package name '{name}' is invalid: {reason}. Valid names contain only alphanumeric characters, hyphens, and underscores.")]
|
|
229
|
+
InvalidPackageName {
|
|
230
|
+
name: String,
|
|
231
|
+
reason: &'static str,
|
|
232
|
+
},
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Don't
|
|
237
|
+
|
|
238
|
+
(Don't) Use generic string errors that lose context.
|
|
239
|
+
```rust
|
|
240
|
+
// Bad: Loses type information and context
|
|
241
|
+
fn load_config(path: &Path) -> Result<Config, String> {
|
|
242
|
+
let content = fs::read_to_string(path)
|
|
243
|
+
.map_err(|e| e.to_string())?; // Context lost!
|
|
244
|
+
|
|
245
|
+
toml::from_str(&content)
|
|
246
|
+
.map_err(|e| e.to_string()) // Which file? What went wrong?
|
|
247
|
+
}
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
(Don't) Discard error information with unwrap or expect in library code.
|
|
251
|
+
```rust
|
|
252
|
+
// Bad: Panics instead of propagating
|
|
253
|
+
fn parse_uri(s: &str) -> Uri {
|
|
254
|
+
Uri::parse(s).unwrap() // Crashes on invalid input!
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Bad: expect without context
|
|
258
|
+
let file = File::open(path).expect("failed"); // Which path?
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
(Don't) Create error types without Display or Debug.
|
|
262
|
+
```rust
|
|
263
|
+
// Bad: Unusable error type
|
|
264
|
+
pub struct MyError {
|
|
265
|
+
code: i32,
|
|
266
|
+
// No Display impl - can't print meaningful message
|
|
267
|
+
}
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
(Don't) Use anyhow/eyre in library code (ok for applications).
|
|
271
|
+
```rust
|
|
272
|
+
// Bad in libraries: Erases type information
|
|
273
|
+
pub fn process() -> anyhow::Result<()> {
|
|
274
|
+
// Callers can't match on specific error variants
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Better for libraries: Concrete error types
|
|
278
|
+
pub fn process() -> Result<(), ProcessError> {
|
|
279
|
+
// Callers can handle specific cases
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
## rust/functions/kleisli-composition
|
|
286
|
+
|
|
287
|
+
Structure functions as `A -> Result<B, E>` (Kleisli arrows) for composable, chainable transformations. This enables powerful composition patterns where each step can fail, following the monadic composition style from Haskell.
|
|
288
|
+
|
|
289
|
+
**Ratings:**
|
|
290
|
+
- Impact: B (enables powerful composition patterns)
|
|
291
|
+
- Feasibility: B (requires thinking in terms of monadic pipelines)
|
|
292
|
+
- Idiomaticity: A (natural with ? and and_then)
|
|
293
|
+
- FP Purity: S (direct Kleisli arrow pattern)
|
|
294
|
+
|
|
295
|
+
### Do
|
|
296
|
+
|
|
297
|
+
(Do) Design functions as Kleisli arrows: `A -> Result<B, E>`.
|
|
298
|
+
```rust
|
|
299
|
+
// Each function is a Kleisli arrow that can be composed
|
|
300
|
+
fn parse_config(input: &str) -> Result<RawConfig, ParseError> { /* ... */ }
|
|
301
|
+
fn validate_config(raw: RawConfig) -> Result<ValidConfig, ValidationError> { /* ... */ }
|
|
302
|
+
fn normalize_config(valid: ValidConfig) -> Result<NormalizedConfig, NormalizeError> { /* ... */ }
|
|
303
|
+
|
|
304
|
+
// Compose them with and_then (Kleisli composition)
|
|
305
|
+
fn load_config(input: &str) -> Result<NormalizedConfig, ConfigError> {
|
|
306
|
+
parse_config(input)
|
|
307
|
+
.map_err(ConfigError::Parse)?
|
|
308
|
+
.pipe(validate_config)
|
|
309
|
+
.map_err(ConfigError::Validation)?
|
|
310
|
+
.pipe(normalize_config)
|
|
311
|
+
.map_err(ConfigError::Normalize)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Or with explicit and_then showing the monadic nature
|
|
315
|
+
fn process_input(s: &str) -> Result<Output, ProcessError> {
|
|
316
|
+
parse(s)
|
|
317
|
+
.and_then(validate)
|
|
318
|
+
.and_then(transform)
|
|
319
|
+
.and_then(finalize)
|
|
320
|
+
}
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
(Do) Use combinators to compose fallible operations.
|
|
324
|
+
```rust
|
|
325
|
+
impl DepSpec {
|
|
326
|
+
pub fn from_value(value: &DependencyValue) -> Result<Self, ParseError> {
|
|
327
|
+
match value {
|
|
328
|
+
DependencyValue::Simple(s) => Self::parse(s),
|
|
329
|
+
DependencyValue::Table(config) => {
|
|
330
|
+
// Chain through optional fields - Kleisli-like
|
|
331
|
+
config.url.as_ref()
|
|
332
|
+
.map(|url| Self::from_url(url, config))
|
|
333
|
+
.or_else(|| config.workspace.as_ref().map(|ws| Ok(DepSpec::Workspace(ws.clone()))))
|
|
334
|
+
.or_else(|| config.path.as_ref().map(|p| Ok(DepSpec::Path(p.clone()))))
|
|
335
|
+
.or_else(|| config.version.as_ref().map(|v| Ok(DepSpec::Version(v.clone()))))
|
|
336
|
+
.unwrap_or_else(|| Ok(DepSpec::Version("*".to_string())))
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
(Do) Create helper traits for method chaining when needed.
|
|
344
|
+
```rust
|
|
345
|
+
trait ResultExt<T, E> {
|
|
346
|
+
fn and_try<U, F>(self, f: F) -> Result<U, E>
|
|
347
|
+
where
|
|
348
|
+
F: FnOnce(T) -> Result<U, E>;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
impl<T, E> ResultExt<T, E> for Result<T, E> {
|
|
352
|
+
fn and_try<U, F>(self, f: F) -> Result<U, E>
|
|
353
|
+
where
|
|
354
|
+
F: FnOnce(T) -> Result<U, E>,
|
|
355
|
+
{
|
|
356
|
+
self.and_then(f)
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Enable fluent chains
|
|
361
|
+
let result = input
|
|
362
|
+
.and_try(step1)
|
|
363
|
+
.and_try(step2)
|
|
364
|
+
.and_try(step3);
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
(Do) Use the pipe pattern for readability.
|
|
368
|
+
```rust
|
|
369
|
+
// With a pipe trait or tap crate
|
|
370
|
+
trait Pipe: Sized {
|
|
371
|
+
fn pipe<F, R>(self, f: F) -> R where F: FnOnce(Self) -> R {
|
|
372
|
+
f(self)
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
impl<T> Pipe for T {}
|
|
376
|
+
|
|
377
|
+
// Reads left-to-right like Unix pipes
|
|
378
|
+
let result = input
|
|
379
|
+
.pipe(parse)
|
|
380
|
+
.and_then(|x| x.pipe(validate))
|
|
381
|
+
.and_then(|x| x.pipe(transform));
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
### Don't
|
|
385
|
+
|
|
386
|
+
(Don't) Mix side effects into pure transformation chains.
|
|
387
|
+
```rust
|
|
388
|
+
// Bad: Side effects hidden in chain
|
|
389
|
+
fn process(input: &str) -> Result<Output, Error> {
|
|
390
|
+
parse(input)
|
|
391
|
+
.and_then(|x| {
|
|
392
|
+
println!("Parsed: {:?}", x); // Side effect!
|
|
393
|
+
log_to_file(&x)?; // More side effects!
|
|
394
|
+
validate(x)
|
|
395
|
+
})
|
|
396
|
+
.and_then(transform)
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Better: Separate side effects from transformations
|
|
400
|
+
fn process(input: &str) -> Result<Output, Error> {
|
|
401
|
+
let parsed = parse(input)?;
|
|
402
|
+
log_parsed(&parsed); // Explicit side effect
|
|
403
|
+
|
|
404
|
+
let validated = validate(parsed)?;
|
|
405
|
+
transform(validated)
|
|
406
|
+
}
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
(Don't) Break the chain with early returns when and_then works.
|
|
410
|
+
```rust
|
|
411
|
+
// Bad: Breaks the monadic flow
|
|
412
|
+
fn process(input: &str) -> Result<Output, Error> {
|
|
413
|
+
let parsed = parse(input)?;
|
|
414
|
+
if !is_valid(&parsed) {
|
|
415
|
+
return Err(Error::Invalid);
|
|
416
|
+
}
|
|
417
|
+
let transformed = transform(parsed)?;
|
|
418
|
+
if transformed.is_empty() {
|
|
419
|
+
return Err(Error::Empty);
|
|
420
|
+
}
|
|
421
|
+
Ok(finalize(transformed))
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Better: Keep the monadic chain
|
|
425
|
+
fn process(input: &str) -> Result<Output, Error> {
|
|
426
|
+
parse(input)
|
|
427
|
+
.and_then(|p| is_valid(&p).then_some(p).ok_or(Error::Invalid))
|
|
428
|
+
.and_then(transform)
|
|
429
|
+
.and_then(|t| (!t.is_empty()).then_some(t).ok_or(Error::Empty))
|
|
430
|
+
.map(finalize)
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
(Don't) Use unwrap in the middle of a chain.
|
|
435
|
+
```rust
|
|
436
|
+
// Bad: Panics break the monadic abstraction
|
|
437
|
+
let result = items.iter()
|
|
438
|
+
.map(|x| parse(x).unwrap()) // Panic!
|
|
439
|
+
.filter(|x| x.is_valid())
|
|
440
|
+
.collect();
|
|
441
|
+
```
|
|
442
|
+
|
|
443
|
+
---
|
|
444
|
+
|
|
445
|
+
## rust/iterators/combinator-pipelines
|
|
446
|
+
|
|
447
|
+
Prefer iterator combinators (map, filter, flat_map, collect) over imperative loops for data transformations. This functional style is more declarative, composable, and often more performant due to lazy evaluation and compiler optimizations.
|
|
448
|
+
|
|
449
|
+
**Ratings:**
|
|
450
|
+
- Impact: A (more declarative, composable, often faster)
|
|
451
|
+
- Feasibility: A (Rust iterators are excellent)
|
|
452
|
+
- Idiomaticity: S (core Rust idiom)
|
|
453
|
+
- FP Purity: S (direct FP; lazy evaluation, composable)
|
|
454
|
+
|
|
455
|
+
### Do
|
|
456
|
+
|
|
457
|
+
(Do) Use iterator chains for data transformations.
|
|
458
|
+
```rust
|
|
459
|
+
fn get_installed_packages() -> Result<Vec<String>, IoError> {
|
|
460
|
+
let packages_path = sem_packages_dir()?;
|
|
461
|
+
|
|
462
|
+
if !packages_path.exists() {
|
|
463
|
+
return Ok(vec![]);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
let mut packages: Vec<String> = fs::read_dir(&packages_path)?
|
|
467
|
+
.flatten() // Result<DirEntry> -> DirEntry (skips errors)
|
|
468
|
+
.filter(|entry| entry.path().is_dir() || entry.path().is_symlink())
|
|
469
|
+
.filter_map(|entry| {
|
|
470
|
+
entry.path()
|
|
471
|
+
.file_name()
|
|
472
|
+
.map(|n| n.to_string_lossy().to_string())
|
|
473
|
+
})
|
|
474
|
+
.collect();
|
|
475
|
+
|
|
476
|
+
packages.sort();
|
|
477
|
+
Ok(packages)
|
|
478
|
+
}
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
(Do) Use flat_map for one-to-many transformations.
|
|
482
|
+
```rust
|
|
483
|
+
fn get_all_dependencies(packages: &[Package]) -> Vec<Dependency> {
|
|
484
|
+
packages.iter()
|
|
485
|
+
.flat_map(|pkg| pkg.dependencies())
|
|
486
|
+
.collect()
|
|
487
|
+
}
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
(Do) Use filter_map to combine filter and map operations.
|
|
491
|
+
```rust
|
|
492
|
+
fn parse_valid_numbers(strings: &[&str]) -> Vec<i32> {
|
|
493
|
+
strings.iter()
|
|
494
|
+
.filter_map(|s| s.parse::<i32>().ok())
|
|
495
|
+
.collect()
|
|
496
|
+
}
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
(Do) Use fold/reduce for accumulating results.
|
|
500
|
+
```rust
|
|
501
|
+
fn total_size(files: &[PathBuf]) -> u64 {
|
|
502
|
+
files.iter()
|
|
503
|
+
.filter_map(|p| fs::metadata(p).ok())
|
|
504
|
+
.map(|m| m.len())
|
|
505
|
+
.sum()
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
fn merge_configs(configs: &[Config]) -> Config {
|
|
509
|
+
configs.iter()
|
|
510
|
+
.fold(Config::default(), |acc, cfg| acc.merge(cfg))
|
|
511
|
+
}
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
(Do) Chain multiple operations for complex transformations.
|
|
515
|
+
```rust
|
|
516
|
+
fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>> {
|
|
517
|
+
entries.iter()
|
|
518
|
+
.filter(|e| e.level >= LogLevel::Warning)
|
|
519
|
+
.filter(|e| e.timestamp > cutoff_time)
|
|
520
|
+
.fold(HashMap::new(), |mut acc, entry| {
|
|
521
|
+
acc.entry(entry.source.clone())
|
|
522
|
+
.or_default()
|
|
523
|
+
.push(entry);
|
|
524
|
+
acc
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
(Do) Use collect with turbofish for type-driven collection.
|
|
530
|
+
```rust
|
|
531
|
+
// Collect into different types based on need
|
|
532
|
+
let vec: Vec<_> = iter.collect();
|
|
533
|
+
let set: HashSet<_> = iter.collect();
|
|
534
|
+
let map: HashMap<_, _> = iter.map(|x| (x.id, x)).collect();
|
|
535
|
+
|
|
536
|
+
// Collect Results - fails fast on first error
|
|
537
|
+
let results: Result<Vec<_>, _> = items.iter()
|
|
538
|
+
.map(|item| process(item))
|
|
539
|
+
.collect();
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Don't
|
|
543
|
+
|
|
544
|
+
(Don't) Use imperative loops when combinators are clearer.
|
|
545
|
+
```rust
|
|
546
|
+
// Bad: Imperative style obscures intent
|
|
547
|
+
fn get_names(users: &[User]) -> Vec<String> {
|
|
548
|
+
let mut names = Vec::new();
|
|
549
|
+
for user in users {
|
|
550
|
+
if user.is_active {
|
|
551
|
+
names.push(user.name.clone());
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
names
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// Good: Declarative style
|
|
558
|
+
fn get_names(users: &[User]) -> Vec<String> {
|
|
559
|
+
users.iter()
|
|
560
|
+
.filter(|u| u.is_active)
|
|
561
|
+
.map(|u| u.name.clone())
|
|
562
|
+
.collect()
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
(Don't) Collect intermediate results unnecessarily.
|
|
567
|
+
```rust
|
|
568
|
+
// Bad: Unnecessary allocation
|
|
569
|
+
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
|
570
|
+
let mapped: Vec<_> = filtered.iter().map(|x| x.value).collect();
|
|
571
|
+
let result: i32 = mapped.iter().sum();
|
|
572
|
+
|
|
573
|
+
// Good: Single lazy chain
|
|
574
|
+
let result: i32 = items.iter()
|
|
575
|
+
.filter(|x| x.valid)
|
|
576
|
+
.map(|x| x.value)
|
|
577
|
+
.sum();
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
(Don't) Use for loops just to build up a Vec.
|
|
581
|
+
```rust
|
|
582
|
+
// Bad: Manual Vec building
|
|
583
|
+
let mut results = Vec::new();
|
|
584
|
+
for item in items {
|
|
585
|
+
results.push(transform(item));
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Good: Use map and collect
|
|
589
|
+
let results: Vec<_> = items.iter().map(transform).collect();
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
(Don't) Nest loops when flat_map works.
|
|
593
|
+
```rust
|
|
594
|
+
// Bad: Nested loops
|
|
595
|
+
let mut all_items = Vec::new();
|
|
596
|
+
for container in containers {
|
|
597
|
+
for item in container.items() {
|
|
598
|
+
all_items.push(item);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Good: flat_map
|
|
603
|
+
let all_items: Vec<_> = containers.iter()
|
|
604
|
+
.flat_map(|c| c.items())
|
|
605
|
+
.collect();
|
|
606
|
+
```
|
|
607
|
+
|
|
608
|
+
---
|
|
609
|
+
|
|
610
|
+
## rust/option/explicit-absence
|
|
611
|
+
|
|
612
|
+
Use Option explicitly and idiomatically; prefer Option<T> combinators (map, and_then, unwrap_or, ok_or) over null-like patterns. Option is Rust's Maybe monad - use it as such.
|
|
613
|
+
|
|
614
|
+
**Ratings:**
|
|
615
|
+
- Impact: A (eliminates null-related bugs)
|
|
616
|
+
- Feasibility: S (Rust enforces this)
|
|
617
|
+
- Idiomaticity: S (fundamental Rust pattern)
|
|
618
|
+
- FP Purity: S (Maybe monad equivalent)
|
|
619
|
+
|
|
620
|
+
### Do
|
|
621
|
+
|
|
622
|
+
(Do) Use Option combinators for clean transformations.
|
|
623
|
+
```rust
|
|
624
|
+
impl Manifest {
|
|
625
|
+
pub fn display_name(&self) -> Option<String> {
|
|
626
|
+
self.workspace.as_ref()
|
|
627
|
+
.and_then(|ws| ws.name.clone())
|
|
628
|
+
.or_else(|| self.package.as_ref().map(|p| p.name.clone()))
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
pub fn version(&self) -> Option<String> {
|
|
632
|
+
self.package.as_ref().map(|p| p.version.clone())
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
pub fn members(&self) -> Option<Vec<String>> {
|
|
636
|
+
self.workspace.as_ref().map(|w| w.members.clone())
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
(Do) Use unwrap_or and unwrap_or_else for defaults.
|
|
642
|
+
```rust
|
|
643
|
+
fn get_config_value(key: &str) -> String {
|
|
644
|
+
config.get(key)
|
|
645
|
+
.cloned()
|
|
646
|
+
.unwrap_or_else(|| default_for_key(key))
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
fn get_timeout() -> Duration {
|
|
650
|
+
env::var("TIMEOUT")
|
|
651
|
+
.ok()
|
|
652
|
+
.and_then(|s| s.parse().ok())
|
|
653
|
+
.unwrap_or(Duration::from_secs(30))
|
|
654
|
+
}
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
(Do) Use ok_or to convert Option to Result.
|
|
658
|
+
```rust
|
|
659
|
+
fn find_package(name: &str) -> Result<&Package, PackageError> {
|
|
660
|
+
packages.get(name)
|
|
661
|
+
.ok_or_else(|| PackageError::NotFound(name.to_string()))
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
fn get_required_field<'a>(map: &'a HashMap<String, Value>, key: &str) -> Result<&'a Value, ConfigError> {
|
|
665
|
+
map.get(key)
|
|
666
|
+
.ok_or(ConfigError::MissingField(key.to_string()))
|
|
667
|
+
}
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
(Do) Use filter and filter_map for conditional processing.
|
|
671
|
+
```rust
|
|
672
|
+
fn find_active_user(id: UserId) -> Option<User> {
|
|
673
|
+
users.get(&id).filter(|u| u.is_active)
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fn get_valid_entries(entries: &[Entry]) -> Vec<&Entry> {
|
|
677
|
+
entries.iter()
|
|
678
|
+
.filter(|e| e.is_valid())
|
|
679
|
+
.collect()
|
|
680
|
+
}
|
|
681
|
+
```
|
|
682
|
+
|
|
683
|
+
(Do) Use the ? operator with Option in functions returning Option.
|
|
684
|
+
```rust
|
|
685
|
+
fn get_nested_value(data: &Data) -> Option<&str> {
|
|
686
|
+
let section = data.sections.get("main")?;
|
|
687
|
+
let item = section.items.first()?;
|
|
688
|
+
item.value.as_deref()
|
|
689
|
+
}
|
|
690
|
+
```
|
|
691
|
+
|
|
692
|
+
### Don't
|
|
693
|
+
|
|
694
|
+
(Don't) Use sentinel values instead of Option.
|
|
695
|
+
```rust
|
|
696
|
+
// Bad: Magic values
|
|
697
|
+
fn find_index(items: &[Item], target: &Item) -> i32 {
|
|
698
|
+
// Returns -1 if not found - caller might forget to check!
|
|
699
|
+
items.iter().position(|i| i == target).map(|i| i as i32).unwrap_or(-1)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
// Good: Explicit Option
|
|
703
|
+
fn find_index(items: &[Item], target: &Item) -> Option<usize> {
|
|
704
|
+
items.iter().position(|i| i == target)
|
|
705
|
+
}
|
|
706
|
+
```
|
|
707
|
+
|
|
708
|
+
(Don't) Overuse unwrap() or expect() outside of tests.
|
|
709
|
+
```rust
|
|
710
|
+
// Bad: Panics on None
|
|
711
|
+
let user = users.get(id).unwrap();
|
|
712
|
+
let name = user.name.as_ref().unwrap();
|
|
713
|
+
|
|
714
|
+
// Good: Handle absence explicitly
|
|
715
|
+
let user = users.get(id).ok_or(UserError::NotFound(id))?;
|
|
716
|
+
let name = user.name.as_deref().unwrap_or("Anonymous");
|
|
717
|
+
```
|
|
718
|
+
|
|
719
|
+
(Don't) Check is_some/is_none then unwrap.
|
|
720
|
+
```rust
|
|
721
|
+
// Bad: Redundant check
|
|
722
|
+
if value.is_some() {
|
|
723
|
+
let v = value.unwrap(); // We just checked!
|
|
724
|
+
process(v);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Good: Use if let or map
|
|
728
|
+
if let Some(v) = value {
|
|
729
|
+
process(v);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
// Or even better for side effects
|
|
733
|
+
value.map(process);
|
|
734
|
+
```
|
|
735
|
+
|
|
736
|
+
(Don't) Create deeply nested Option chains without combinators.
|
|
737
|
+
```rust
|
|
738
|
+
// Bad: Nested matching
|
|
739
|
+
match outer {
|
|
740
|
+
Some(o) => match o.inner {
|
|
741
|
+
Some(i) => match i.value {
|
|
742
|
+
Some(v) => Some(transform(v)),
|
|
743
|
+
None => None,
|
|
744
|
+
},
|
|
745
|
+
None => None,
|
|
746
|
+
},
|
|
747
|
+
None => None,
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
// Good: Combinator chain
|
|
751
|
+
outer
|
|
752
|
+
.and_then(|o| o.inner)
|
|
753
|
+
.and_then(|i| i.value)
|
|
754
|
+
.map(transform)
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
---
|
|
758
|
+
|
|
759
|
+
## rust/testing/property-based
|
|
760
|
+
|
|
761
|
+
Complement unit tests with property-based testing to verify invariants across many generated inputs. This QuickCheck-inspired approach catches edge cases that example-based tests miss.
|
|
762
|
+
|
|
763
|
+
**Ratings:**
|
|
764
|
+
- Impact: A (catches edge cases unit tests miss)
|
|
765
|
+
- Feasibility: B (requires proptest/quickcheck; learning curve)
|
|
766
|
+
- Idiomaticity: A (growing Rust practice)
|
|
767
|
+
- FP Purity: S (QuickCheck heritage; declarative testing)
|
|
768
|
+
|
|
769
|
+
### Do
|
|
770
|
+
|
|
771
|
+
(Do) Use proptest for property-based testing.
|
|
772
|
+
```rust
|
|
773
|
+
use proptest::prelude::*;
|
|
774
|
+
|
|
775
|
+
proptest! {
|
|
776
|
+
#[test]
|
|
777
|
+
fn parse_roundtrip(s in "[a-z][a-z0-9_-]{0,63}") {
|
|
778
|
+
let name = PackageName::new(&s).unwrap();
|
|
779
|
+
assert_eq!(name.as_str(), s);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
#[test]
|
|
783
|
+
fn version_ordering(a in 0u32..1000, b in 0u32..1000, c in 0u32..1000,
|
|
784
|
+
x in 0u32..1000, y in 0u32..1000, z in 0u32..1000) {
|
|
785
|
+
let v1 = Version::new(a, b, c);
|
|
786
|
+
let v2 = Version::new(x, y, z);
|
|
787
|
+
|
|
788
|
+
// Ordering should be consistent
|
|
789
|
+
if v1 < v2 {
|
|
790
|
+
assert!(v2 > v1);
|
|
791
|
+
assert!(v1 <= v2);
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
```
|
|
796
|
+
|
|
797
|
+
(Do) Test algebraic properties (identity, associativity, commutativity).
|
|
798
|
+
```rust
|
|
799
|
+
proptest! {
|
|
800
|
+
#[test]
|
|
801
|
+
fn merge_associative(a: Config, b: Config, c: Config) {
|
|
802
|
+
// (a.merge(b)).merge(c) == a.merge(b.merge(c))
|
|
803
|
+
let left = a.clone().merge(&b).merge(&c);
|
|
804
|
+
let right = a.merge(&b.merge(&c));
|
|
805
|
+
assert_eq!(left, right);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
#[test]
|
|
809
|
+
fn merge_identity(config: Config) {
|
|
810
|
+
// config.merge(empty) == config
|
|
811
|
+
let merged = config.clone().merge(&Config::default());
|
|
812
|
+
assert_eq!(merged, config);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
```
|
|
816
|
+
|
|
817
|
+
(Do) Test invariants that should hold for all valid inputs.
|
|
818
|
+
```rust
|
|
819
|
+
proptest! {
|
|
820
|
+
#[test]
|
|
821
|
+
fn validated_email_contains_at(s in ".+@.+\\..+") {
|
|
822
|
+
if let Ok(email) = Email::parse(&s) {
|
|
823
|
+
assert!(email.as_str().contains('@'));
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
#[test]
|
|
828
|
+
fn uri_resolve_compact_roundtrip(prefix in "[a-z]+", local in "[a-zA-Z][a-zA-Z0-9]*") {
|
|
829
|
+
let prefixed = format!("{}:{}", prefix, local);
|
|
830
|
+
let map = PrefixMap::default();
|
|
831
|
+
|
|
832
|
+
if let Some(resolved) = map.resolve(&prefixed) {
|
|
833
|
+
let compacted = map.compact(&resolved);
|
|
834
|
+
// Should roundtrip (or return full URI if prefix unknown)
|
|
835
|
+
assert!(compacted == prefixed || compacted == resolved);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
```
|
|
840
|
+
|
|
841
|
+
(Do) Use custom strategies for domain-specific types.
|
|
842
|
+
```rust
|
|
843
|
+
fn valid_package_name() -> impl Strategy<Value = String> {
|
|
844
|
+
"[a-z][a-z0-9-]{0,62}[a-z0-9]?"
|
|
845
|
+
.prop_filter("Must not have consecutive hyphens", |s| !s.contains("--"))
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
fn valid_version() -> impl Strategy<Value = Version> {
|
|
849
|
+
(0u32..1000, 0u32..1000, 0u32..1000)
|
|
850
|
+
.prop_map(|(major, minor, patch)| Version::new(major, minor, patch))
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
proptest! {
|
|
854
|
+
#[test]
|
|
855
|
+
fn package_names_are_valid(name in valid_package_name()) {
|
|
856
|
+
assert!(PackageName::new(&name).is_ok());
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
```
|
|
860
|
+
|
|
861
|
+
### Don't
|
|
862
|
+
|
|
863
|
+
(Don't) Only write example-based unit tests for complex logic.
|
|
864
|
+
```rust
|
|
865
|
+
// Incomplete: Only tests a few examples
|
|
866
|
+
#[test]
|
|
867
|
+
fn test_merge() {
|
|
868
|
+
let a = Config { timeout: 10, retries: 3 };
|
|
869
|
+
let b = Config { timeout: 20, retries: 5 };
|
|
870
|
+
let merged = a.merge(&b);
|
|
871
|
+
assert_eq!(merged.timeout, 20);
|
|
872
|
+
// What about edge cases? Overflow? Default values?
|
|
873
|
+
}
|
|
874
|
+
```
|
|
875
|
+
|
|
876
|
+
(Don't) Ignore test failures without understanding the counterexample.
|
|
877
|
+
```rust
|
|
878
|
+
// Bad: Ignoring failures
|
|
879
|
+
proptest! {
|
|
880
|
+
#[test]
|
|
881
|
+
#[ignore] // "It fails sometimes, I'll fix it later"
|
|
882
|
+
fn flaky_property(x: i32) {
|
|
883
|
+
// ...
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
```
|
|
887
|
+
|
|
888
|
+
(Don't) Write properties that are too weak or tautological.
|
|
889
|
+
```rust
|
|
890
|
+
// Bad: This always passes, tests nothing useful
|
|
891
|
+
proptest! {
|
|
892
|
+
#[test]
|
|
893
|
+
fn useless_property(x: i32) {
|
|
894
|
+
let result = process(x);
|
|
895
|
+
assert!(result == result); // Tautology!
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
// Bad: Property is just the implementation
|
|
900
|
+
proptest! {
|
|
901
|
+
#[test]
|
|
902
|
+
fn reimplements_function(a: i32, b: i32) {
|
|
903
|
+
assert_eq!(add(a, b), a + b); // Just restates the implementation
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
```
|
|
907
|
+
|
|
908
|
+
(Don't) Generate invalid inputs without proper filtering.
|
|
909
|
+
```rust
|
|
910
|
+
// Bad: Generates invalid UTF-8 and panics
|
|
911
|
+
proptest! {
|
|
912
|
+
#[test]
|
|
913
|
+
fn bad_string_test(bytes: Vec<u8>) {
|
|
914
|
+
let s = String::from_utf8(bytes).unwrap(); // Panics on invalid UTF-8!
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Good: Use proper string strategies
|
|
919
|
+
proptest! {
|
|
920
|
+
#[test]
|
|
921
|
+
fn good_string_test(s: String) { // proptest generates valid strings
|
|
922
|
+
// ...
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
```
|
|
926
|
+
|
|
927
|
+
---
|
|
928
|
+
|
|
929
|
+
## rust/testing/tdd-red-green-refactor
|
|
930
|
+
|
|
931
|
+
Follow the strict TDD cycle: write a failing test first (red), implement minimally to pass (green), then refactor. This discipline ensures testability, prevents regression, and drives better design.
|
|
932
|
+
|
|
933
|
+
**Ratings:**
|
|
934
|
+
- Impact: S (ensures testability, prevents regression)
|
|
935
|
+
- Feasibility: A (Rust's cargo test makes this easy)
|
|
936
|
+
- Idiomaticity: A (industry best practice)
|
|
937
|
+
- FP Purity: B (supports referential transparency goals)
|
|
938
|
+
|
|
939
|
+
### Do
|
|
940
|
+
|
|
941
|
+
(Do) Write the test first, watch it fail.
|
|
942
|
+
```rust
|
|
943
|
+
// Step 1: RED - Write failing test
|
|
944
|
+
#[cfg(test)]
|
|
945
|
+
mod tests {
|
|
946
|
+
use super::*;
|
|
947
|
+
|
|
948
|
+
#[test]
|
|
949
|
+
fn parse_valid_semver() {
|
|
950
|
+
let version = Version::parse("1.2.3").unwrap();
|
|
951
|
+
assert_eq!(version.major, 1);
|
|
952
|
+
assert_eq!(version.minor, 2);
|
|
953
|
+
assert_eq!(version.patch, 3);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
// At this point: cargo test fails - Version doesn't exist yet!
|
|
958
|
+
```
|
|
959
|
+
|
|
960
|
+
(Do) Implement the minimum code to pass.
|
|
961
|
+
```rust
|
|
962
|
+
// Step 2: GREEN - Minimal implementation
|
|
963
|
+
pub struct Version {
|
|
964
|
+
pub major: u32,
|
|
965
|
+
pub minor: u32,
|
|
966
|
+
pub patch: u32,
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
impl Version {
|
|
970
|
+
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
|
971
|
+
let parts: Vec<&str> = s.split('.').collect();
|
|
972
|
+
if parts.len() != 3 {
|
|
973
|
+
return Err(ParseError::InvalidFormat);
|
|
974
|
+
}
|
|
975
|
+
Ok(Self {
|
|
976
|
+
major: parts[0].parse().map_err(|_| ParseError::InvalidNumber)?,
|
|
977
|
+
minor: parts[1].parse().map_err(|_| ParseError::InvalidNumber)?,
|
|
978
|
+
patch: parts[2].parse().map_err(|_| ParseError::InvalidNumber)?,
|
|
979
|
+
})
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// cargo test passes!
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
(Do) Refactor while keeping tests green.
|
|
987
|
+
```rust
|
|
988
|
+
// Step 3: REFACTOR - Improve design
|
|
989
|
+
impl Version {
|
|
990
|
+
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
|
991
|
+
let mut parts = s.splitn(3, '.');
|
|
992
|
+
|
|
993
|
+
let major = Self::parse_component(parts.next())?;
|
|
994
|
+
let minor = Self::parse_component(parts.next())?;
|
|
995
|
+
let patch = Self::parse_component(parts.next())?;
|
|
996
|
+
|
|
997
|
+
if parts.next().is_some() {
|
|
998
|
+
return Err(ParseError::TooManyComponents);
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
Ok(Self { major, minor, patch })
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
fn parse_component(s: Option<&str>) -> Result<u32, ParseError> {
|
|
1005
|
+
s.ok_or(ParseError::MissingComponent)?
|
|
1006
|
+
.parse()
|
|
1007
|
+
.map_err(|_| ParseError::InvalidNumber)
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
// Tests still pass after refactoring!
|
|
1012
|
+
```
|
|
1013
|
+
|
|
1014
|
+
(Do) Add tests for edge cases incrementally.
|
|
1015
|
+
```rust
|
|
1016
|
+
#[test]
|
|
1017
|
+
fn parse_rejects_empty_string() {
|
|
1018
|
+
assert!(Version::parse("").is_err());
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
#[test]
|
|
1022
|
+
fn parse_rejects_non_numeric() {
|
|
1023
|
+
assert!(Version::parse("a.b.c").is_err());
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
#[test]
|
|
1027
|
+
fn parse_rejects_negative_numbers() {
|
|
1028
|
+
assert!(Version::parse("-1.0.0").is_err());
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
#[test]
|
|
1032
|
+
fn parse_handles_leading_zeros() {
|
|
1033
|
+
let v = Version::parse("01.02.03").unwrap();
|
|
1034
|
+
assert_eq!(v.major, 1); // Decide: allow or reject?
|
|
1035
|
+
}
|
|
1036
|
+
```
|
|
1037
|
+
|
|
1038
|
+
(Do) Use test modules colocated with implementation.
|
|
1039
|
+
```rust
|
|
1040
|
+
// src/version.rs
|
|
1041
|
+
pub struct Version { /* ... */ }
|
|
1042
|
+
|
|
1043
|
+
impl Version {
|
|
1044
|
+
pub fn parse(s: &str) -> Result<Self, ParseError> { /* ... */ }
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
#[cfg(test)]
|
|
1048
|
+
mod tests {
|
|
1049
|
+
use super::*;
|
|
1050
|
+
|
|
1051
|
+
// Tests live next to the code they test
|
|
1052
|
+
#[test]
|
|
1053
|
+
fn test_parse() { /* ... */ }
|
|
1054
|
+
}
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### Don't
|
|
1058
|
+
|
|
1059
|
+
(Don't) Write implementation before tests.
|
|
1060
|
+
```rust
|
|
1061
|
+
// Bad: Implementation without tests
|
|
1062
|
+
pub fn complex_algorithm(input: &str) -> Result<Output, Error> {
|
|
1063
|
+
// 200 lines of complex logic
|
|
1064
|
+
// No tests to verify correctness
|
|
1065
|
+
// No tests to prevent regression
|
|
1066
|
+
}
|
|
1067
|
+
```
|
|
1068
|
+
|
|
1069
|
+
(Don't) Write tests that pass trivially or test nothing.
|
|
1070
|
+
```rust
|
|
1071
|
+
// Bad: Test that always passes
|
|
1072
|
+
#[test]
|
|
1073
|
+
fn useless_test() {
|
|
1074
|
+
assert!(true);
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
// Bad: Test that doesn't assert behavior
|
|
1078
|
+
#[test]
|
|
1079
|
+
fn test_without_assertions() {
|
|
1080
|
+
let _ = Version::parse("1.0.0"); // No assertions!
|
|
1081
|
+
}
|
|
1082
|
+
```
|
|
1083
|
+
|
|
1084
|
+
(Don't) Skip the refactor step.
|
|
1085
|
+
```rust
|
|
1086
|
+
// Bad: "It works, ship it!"
|
|
1087
|
+
impl Version {
|
|
1088
|
+
pub fn parse(s: &str) -> Result<Self, ParseError> {
|
|
1089
|
+
// Copy-pasted code, magic numbers, unclear logic
|
|
1090
|
+
// "I'll clean it up later" (narrator: they didn't)
|
|
1091
|
+
let p: Vec<&str> = s.split('.').collect();
|
|
1092
|
+
if p.len() != 3 { return Err(ParseError::X); }
|
|
1093
|
+
let a = p[0].parse::<u32>();
|
|
1094
|
+
let b = p[1].parse::<u32>();
|
|
1095
|
+
let c = p[2].parse::<u32>();
|
|
1096
|
+
if a.is_err() || b.is_err() || c.is_err() { return Err(ParseError::Y); }
|
|
1097
|
+
Ok(Self { major: a.unwrap(), minor: b.unwrap(), patch: c.unwrap() })
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
```
|
|
1101
|
+
|
|
1102
|
+
(Don't) Write tests after the fact that just confirm current behavior.
|
|
1103
|
+
```rust
|
|
1104
|
+
// Bad: "Characterization tests" without understanding intent
|
|
1105
|
+
#[test]
|
|
1106
|
+
fn test_weird_behavior() {
|
|
1107
|
+
// I don't know why it returns 42, but it does, so test it
|
|
1108
|
+
assert_eq!(mysterious_function("input"), 42);
|
|
1109
|
+
}
|
|
1110
|
+
```
|
|
1111
|
+
|
|
1112
|
+
(Don't) Test private implementation details.
|
|
1113
|
+
```rust
|
|
1114
|
+
// Bad: Testing internals that may change
|
|
1115
|
+
#[test]
|
|
1116
|
+
fn test_internal_cache_structure() {
|
|
1117
|
+
let obj = MyStruct::new();
|
|
1118
|
+
// Accessing private fields via unsafe or reflection
|
|
1119
|
+
assert_eq!(obj.internal_cache.len(), 0); // Fragile!
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// Good: Test public behavior
|
|
1123
|
+
#[test]
|
|
1124
|
+
fn test_caching_behavior() {
|
|
1125
|
+
let obj = MyStruct::new();
|
|
1126
|
+
let result1 = obj.expensive_operation();
|
|
1127
|
+
let result2 = obj.expensive_operation(); // Should be cached
|
|
1128
|
+
assert_eq!(result1, result2);
|
|
1129
|
+
}
|
|
1130
|
+
```
|
|
1131
|
+
|
|
1132
|
+
---
|
|
1133
|
+
|
|
1134
|
+
## rust/traits/small-composable
|
|
1135
|
+
|
|
1136
|
+
Prefer small, focused traits that compose well over large monolithic interfaces. This Haskell typeclass-inspired approach enables better abstraction, easier testing, and more flexible code reuse.
|
|
1137
|
+
|
|
1138
|
+
**Ratings:**
|
|
1139
|
+
- Impact: A (better abstraction, easier testing)
|
|
1140
|
+
- Feasibility: B (requires careful API design)
|
|
1141
|
+
- Idiomaticity: S (Rust's trait system shines here)
|
|
1142
|
+
- FP Purity: A (typeclass-inspired composition)
|
|
1143
|
+
|
|
1144
|
+
### Do
|
|
1145
|
+
|
|
1146
|
+
(Do) Design small, single-purpose traits.
|
|
1147
|
+
```rust
|
|
1148
|
+
/// Can be resolved from a prefixed form to a full URI
|
|
1149
|
+
trait Resolvable {
|
|
1150
|
+
fn resolve(&self, prefix_map: &PrefixMap) -> String;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/// Can be compacted from a full URI to a prefixed form
|
|
1154
|
+
trait Compactable {
|
|
1155
|
+
fn compact(&self, prefix_map: &PrefixMap) -> String;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
/// Can be validated against constraints
|
|
1159
|
+
trait Validatable {
|
|
1160
|
+
type Error;
|
|
1161
|
+
fn validate(&self) -> Result<(), Self::Error>;
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/// Can be loaded from a path
|
|
1165
|
+
trait Loadable: Sized {
|
|
1166
|
+
type Error;
|
|
1167
|
+
fn load(path: &Path) -> Result<Self, Self::Error>;
|
|
1168
|
+
}
|
|
1169
|
+
```
|
|
1170
|
+
|
|
1171
|
+
(Do) Compose traits using supertraits and bounds.
|
|
1172
|
+
```rust
|
|
1173
|
+
// Compose small traits into larger capabilities
|
|
1174
|
+
trait UriHandler: Resolvable + Compactable {}
|
|
1175
|
+
|
|
1176
|
+
// Automatic implementation for types that have both
|
|
1177
|
+
impl<T: Resolvable + Compactable> UriHandler for T {}
|
|
1178
|
+
|
|
1179
|
+
// Use trait bounds for flexible functions
|
|
1180
|
+
fn process_uri<T: Resolvable + Display>(uri: &T, map: &PrefixMap) -> String {
|
|
1181
|
+
format!("{} -> {}", uri, uri.resolve(map))
|
|
1182
|
+
}
|
|
1183
|
+
```
|
|
1184
|
+
|
|
1185
|
+
(Do) Use extension traits to add methods to existing types.
|
|
1186
|
+
```rust
|
|
1187
|
+
trait ResultExt<T, E> {
|
|
1188
|
+
fn context(self, msg: &str) -> Result<T, ContextError<E>>;
|
|
1189
|
+
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, ContextError<E>>;
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
impl<T, E> ResultExt<T, E> for Result<T, E> {
|
|
1193
|
+
fn context(self, msg: &str) -> Result<T, ContextError<E>> {
|
|
1194
|
+
self.map_err(|e| ContextError { context: msg.to_string(), source: e })
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
fn with_context<F: FnOnce() -> String>(self, f: F) -> Result<T, ContextError<E>> {
|
|
1198
|
+
self.map_err(|e| ContextError { context: f(), source: e })
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
// Usage
|
|
1203
|
+
let data = fs::read_to_string(path)
|
|
1204
|
+
.context("failed to read config")?;
|
|
1205
|
+
```
|
|
1206
|
+
|
|
1207
|
+
(Do) Implement standard library traits for interoperability.
|
|
1208
|
+
```rust
|
|
1209
|
+
impl Default for SemStore {
|
|
1210
|
+
fn default() -> Self {
|
|
1211
|
+
Self::new().expect("Failed to create default SemStore")
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1215
|
+
impl Display for PackageName {
|
|
1216
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
1217
|
+
write!(f, "{}", self.0)
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
impl FromStr for Version {
|
|
1222
|
+
type Err = ParseVersionError;
|
|
1223
|
+
|
|
1224
|
+
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
1225
|
+
// parsing logic
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
### Don't
|
|
1231
|
+
|
|
1232
|
+
(Don't) Create large, monolithic traits.
|
|
1233
|
+
```rust
|
|
1234
|
+
// Bad: Too many responsibilities
|
|
1235
|
+
trait Repository {
|
|
1236
|
+
fn connect(&mut self) -> Result<(), Error>;
|
|
1237
|
+
fn disconnect(&mut self) -> Result<(), Error>;
|
|
1238
|
+
fn find_by_id(&self, id: &str) -> Result<Entity, Error>;
|
|
1239
|
+
fn find_all(&self) -> Result<Vec<Entity>, Error>;
|
|
1240
|
+
fn save(&mut self, entity: &Entity) -> Result<(), Error>;
|
|
1241
|
+
fn delete(&mut self, id: &str) -> Result<(), Error>;
|
|
1242
|
+
fn begin_transaction(&mut self) -> Result<Transaction, Error>;
|
|
1243
|
+
fn commit(&mut self) -> Result<(), Error>;
|
|
1244
|
+
fn rollback(&mut self) -> Result<(), Error>;
|
|
1245
|
+
fn execute_query(&self, query: &str) -> Result<QueryResult, Error>;
|
|
1246
|
+
// ... 20 more methods
|
|
1247
|
+
}
|
|
1248
|
+
```
|
|
1249
|
+
|
|
1250
|
+
(Don't) Use trait objects when generics suffice.
|
|
1251
|
+
```rust
|
|
1252
|
+
// Bad: Unnecessary dynamic dispatch
|
|
1253
|
+
fn process(items: &[Box<dyn Processable>]) {
|
|
1254
|
+
for item in items {
|
|
1255
|
+
item.process();
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
// Good: Static dispatch with generics
|
|
1260
|
+
fn process<T: Processable>(items: &[T]) {
|
|
1261
|
+
for item in items {
|
|
1262
|
+
item.process();
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
(Don't) Require unused trait methods via blanket requirements.
|
|
1268
|
+
```rust
|
|
1269
|
+
// Bad: Forces implementers to provide unused methods
|
|
1270
|
+
trait DataStore: Connect + Query + Mutate + Transaction + Cache + Log {
|
|
1271
|
+
// Most implementers don't need all of these
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
// Good: Compose only what's needed
|
|
1275
|
+
fn process<T: Query + Mutate>(store: &mut T) {
|
|
1276
|
+
// Only requires the capabilities actually used
|
|
1277
|
+
}
|
|
1278
|
+
```
|
|
1279
|
+
|
|
1280
|
+
(Don't) Use associated types when generic parameters work better.
|
|
1281
|
+
```rust
|
|
1282
|
+
// Bad: Can only have one implementation per type
|
|
1283
|
+
trait Container {
|
|
1284
|
+
type Item;
|
|
1285
|
+
fn get(&self) -> &Self::Item;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
// Good: Allows multiple implementations
|
|
1289
|
+
trait Container<T> {
|
|
1290
|
+
fn get(&self) -> &T;
|
|
1291
|
+
}
|
|
1292
|
+
// Now Vec<i32> can be Container<i32> AND Container<String> if needed
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
---
|
|
1296
|
+
|
|
1297
|
+
## rust/types/discriminated-unions
|
|
1298
|
+
|
|
1299
|
+
Model state and variants with enums (algebraic sum types) rather than flags, inheritance, or stringly-typed values. Exhaustive pattern matching ensures all cases are handled at compile time - Rust's killer feature borrowed from ML/Haskell.
|
|
1300
|
+
|
|
1301
|
+
**Ratings:**
|
|
1302
|
+
- Impact: S (exhaustive matching prevents bugs)
|
|
1303
|
+
- Feasibility: A (native Rust feature)
|
|
1304
|
+
- Idiomaticity: S (Rust's killer feature)
|
|
1305
|
+
- FP Purity: S (algebraic data types; Haskell-equivalent)
|
|
1306
|
+
|
|
1307
|
+
### Do
|
|
1308
|
+
|
|
1309
|
+
(Do) Use enums to model mutually exclusive states.
|
|
1310
|
+
```rust
|
|
1311
|
+
#[derive(Debug, Clone, PartialEq)]
|
|
1312
|
+
pub enum DepSpec {
|
|
1313
|
+
Workspace(String),
|
|
1314
|
+
Path(String),
|
|
1315
|
+
Version(String),
|
|
1316
|
+
Url {
|
|
1317
|
+
url: String,
|
|
1318
|
+
version: Option<String>,
|
|
1319
|
+
include: Option<Vec<String>>,
|
|
1320
|
+
exclude: Option<Vec<String>>,
|
|
1321
|
+
},
|
|
1322
|
+
}
|
|
1323
|
+
|
|
1324
|
+
impl DepSpec {
|
|
1325
|
+
pub fn resolve(&self, workspace_root: &Path) -> Result<PathBuf, ResolveError> {
|
|
1326
|
+
match self {
|
|
1327
|
+
DepSpec::Workspace(name) => resolve_workspace_member(workspace_root, name),
|
|
1328
|
+
DepSpec::Path(path) => Ok(workspace_root.join(path)),
|
|
1329
|
+
DepSpec::Version(ver) => fetch_from_registry(ver),
|
|
1330
|
+
DepSpec::Url { url, .. } => fetch_from_url(url),
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
```
|
|
1335
|
+
|
|
1336
|
+
(Do) Use enums for state machines with compile-time guarantees.
|
|
1337
|
+
```rust
|
|
1338
|
+
enum ConnectionState {
|
|
1339
|
+
Disconnected,
|
|
1340
|
+
Connecting { attempt: u32 },
|
|
1341
|
+
Connected { session_id: String },
|
|
1342
|
+
Error { message: String, retries: u32 },
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
impl ConnectionState {
|
|
1346
|
+
fn can_send(&self) -> bool {
|
|
1347
|
+
matches!(self, ConnectionState::Connected { .. })
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
fn transition(self, event: Event) -> Self {
|
|
1351
|
+
match (self, event) {
|
|
1352
|
+
(ConnectionState::Disconnected, Event::Connect) =>
|
|
1353
|
+
ConnectionState::Connecting { attempt: 1 },
|
|
1354
|
+
(ConnectionState::Connecting { .. }, Event::Success(id)) =>
|
|
1355
|
+
ConnectionState::Connected { session_id: id },
|
|
1356
|
+
(ConnectionState::Connecting { attempt }, Event::Failure(msg)) if attempt < 3 =>
|
|
1357
|
+
ConnectionState::Connecting { attempt: attempt + 1 },
|
|
1358
|
+
(ConnectionState::Connecting { attempt }, Event::Failure(msg)) =>
|
|
1359
|
+
ConnectionState::Error { message: msg, retries: attempt },
|
|
1360
|
+
(state, _) => state, // Invalid transitions are no-ops
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
(Do) Use enums to make invalid states unrepresentable.
|
|
1367
|
+
```rust
|
|
1368
|
+
// User can be either anonymous or authenticated, never both
|
|
1369
|
+
enum User {
|
|
1370
|
+
Anonymous,
|
|
1371
|
+
Authenticated {
|
|
1372
|
+
id: UserId,
|
|
1373
|
+
email: Email,
|
|
1374
|
+
roles: Vec<Role>,
|
|
1375
|
+
},
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
// A form field is either pristine, touched, or submitted
|
|
1379
|
+
enum FieldState<T> {
|
|
1380
|
+
Pristine,
|
|
1381
|
+
Touched { value: T, errors: Vec<ValidationError> },
|
|
1382
|
+
Submitted { value: T },
|
|
1383
|
+
}
|
|
1384
|
+
```
|
|
1385
|
+
|
|
1386
|
+
(Do) Leverage exhaustive matching for safety.
|
|
1387
|
+
```rust
|
|
1388
|
+
fn handle_result(result: QueryResult) -> Response {
|
|
1389
|
+
match result {
|
|
1390
|
+
QueryResult::Success(data) => Response::json(data),
|
|
1391
|
+
QueryResult::NotFound => Response::status(404),
|
|
1392
|
+
QueryResult::Unauthorized => Response::status(401),
|
|
1393
|
+
QueryResult::RateLimited { retry_after } => {
|
|
1394
|
+
Response::status(429).header("Retry-After", retry_after)
|
|
1395
|
+
}
|
|
1396
|
+
// Compiler error if we forget a variant!
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
```
|
|
1400
|
+
|
|
1401
|
+
### Don't
|
|
1402
|
+
|
|
1403
|
+
(Don't) Use boolean flags for mutually exclusive states.
|
|
1404
|
+
```rust
|
|
1405
|
+
// Bad: Multiple bools can have invalid combinations
|
|
1406
|
+
struct User {
|
|
1407
|
+
is_anonymous: bool,
|
|
1408
|
+
is_authenticated: bool, // What if both are true?
|
|
1409
|
+
is_admin: bool,
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// Bad: Stringly-typed state
|
|
1413
|
+
struct Connection {
|
|
1414
|
+
state: String, // "connected", "disconnected", typos possible
|
|
1415
|
+
}
|
|
1416
|
+
```
|
|
1417
|
+
|
|
1418
|
+
(Don't) Use Option when you need more than two states.
|
|
1419
|
+
```rust
|
|
1420
|
+
// Bad: Option doesn't capture "loading" vs "error" vs "empty"
|
|
1421
|
+
struct DataView {
|
|
1422
|
+
data: Option<Vec<Item>>, // Is None loading, error, or empty?
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
// Good: Explicit states
|
|
1426
|
+
enum DataState {
|
|
1427
|
+
Loading,
|
|
1428
|
+
Empty,
|
|
1429
|
+
Loaded(Vec<Item>),
|
|
1430
|
+
Error(String),
|
|
1431
|
+
}
|
|
1432
|
+
```
|
|
1433
|
+
|
|
1434
|
+
(Don't) Use inheritance-like patterns with trait objects when enums suffice.
|
|
1435
|
+
```rust
|
|
1436
|
+
// Bad: Runtime dispatch when compile-time would work
|
|
1437
|
+
trait Shape {
|
|
1438
|
+
fn area(&self) -> f64;
|
|
1439
|
+
}
|
|
1440
|
+
struct Circle { radius: f64 }
|
|
1441
|
+
struct Rectangle { width: f64, height: f64 }
|
|
1442
|
+
fn process(shape: &dyn Shape) { /* ... */ }
|
|
1443
|
+
|
|
1444
|
+
// Good: Enum when variants are known
|
|
1445
|
+
enum Shape {
|
|
1446
|
+
Circle { radius: f64 },
|
|
1447
|
+
Rectangle { width: f64, height: f64 },
|
|
1448
|
+
}
|
|
1449
|
+
impl Shape {
|
|
1450
|
+
fn area(&self) -> f64 {
|
|
1451
|
+
match self {
|
|
1452
|
+
Shape::Circle { radius } => std::f64::consts::PI * radius * radius,
|
|
1453
|
+
Shape::Rectangle { width, height } => width * height,
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
```
|
|
1458
|
+
|
|
1459
|
+
(Don't) Use integers or strings as type discriminators.
|
|
1460
|
+
```rust
|
|
1461
|
+
// Bad: Magic numbers
|
|
1462
|
+
const USER_TYPE_ADMIN: i32 = 1;
|
|
1463
|
+
const USER_TYPE_MEMBER: i32 = 2;
|
|
1464
|
+
struct User { user_type: i32 }
|
|
1465
|
+
|
|
1466
|
+
// Bad: Stringly typed
|
|
1467
|
+
struct Message { msg_type: String } // "request", "response", typos!
|
|
1468
|
+
```
|
|
1469
|
+
|
|
1470
|
+
---
|
|
1471
|
+
|
|
1472
|
+
## rust/types/newtype-wrappers
|
|
1473
|
+
|
|
1474
|
+
Use the newtype pattern to create distinct types for domain-specific values, preventing accidental mixing of semantically different data. This Haskell-inspired pattern provides compile-time safety with zero runtime cost.
|
|
1475
|
+
|
|
1476
|
+
**Ratings:**
|
|
1477
|
+
- Impact: A (prevents mixing semantically different values)
|
|
1478
|
+
- Feasibility: B (requires discipline; some boilerplate)
|
|
1479
|
+
- Idiomaticity: S (core Rust pattern, zero-cost abstraction)
|
|
1480
|
+
- FP Purity: A (Haskell-inspired; phantom types possible)
|
|
1481
|
+
|
|
1482
|
+
### Do
|
|
1483
|
+
|
|
1484
|
+
(Do) Wrap primitive types in newtypes for domain semantics.
|
|
1485
|
+
```rust
|
|
1486
|
+
/// A validated package name following sem conventions
|
|
1487
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
1488
|
+
pub struct PackageName(String);
|
|
1489
|
+
|
|
1490
|
+
impl PackageName {
|
|
1491
|
+
pub fn new(name: impl Into<String>) -> Result<Self, ValidationError> {
|
|
1492
|
+
let name = name.into();
|
|
1493
|
+
if name.is_empty() {
|
|
1494
|
+
return Err(ValidationError::EmptyName);
|
|
1495
|
+
}
|
|
1496
|
+
if !name.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
|
|
1497
|
+
return Err(ValidationError::InvalidCharacters);
|
|
1498
|
+
}
|
|
1499
|
+
Ok(Self(name))
|
|
1500
|
+
}
|
|
1501
|
+
|
|
1502
|
+
pub fn as_str(&self) -> &str {
|
|
1503
|
+
&self.0
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
```
|
|
1507
|
+
|
|
1508
|
+
(Do) Use newtypes to distinguish semantically different IDs.
|
|
1509
|
+
```rust
|
|
1510
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
1511
|
+
pub struct UserId(u64);
|
|
1512
|
+
|
|
1513
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
1514
|
+
pub struct OrderId(u64);
|
|
1515
|
+
|
|
1516
|
+
// Compiler prevents mixing:
|
|
1517
|
+
fn process_order(user: UserId, order: OrderId) { /* ... */ }
|
|
1518
|
+
|
|
1519
|
+
// This won't compile:
|
|
1520
|
+
// process_order(order_id, user_id); // Error: expected UserId, found OrderId
|
|
1521
|
+
```
|
|
1522
|
+
|
|
1523
|
+
(Do) Derive common traits to maintain ergonomics.
|
|
1524
|
+
```rust
|
|
1525
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
1526
|
+
#[serde(transparent)]
|
|
1527
|
+
pub struct Uri(String);
|
|
1528
|
+
```
|
|
1529
|
+
|
|
1530
|
+
(Do) Use phantom types for compile-time state tracking.
|
|
1531
|
+
```rust
|
|
1532
|
+
use std::marker::PhantomData;
|
|
1533
|
+
|
|
1534
|
+
struct Validated;
|
|
1535
|
+
struct Unvalidated;
|
|
1536
|
+
|
|
1537
|
+
struct Input<State> {
|
|
1538
|
+
data: String,
|
|
1539
|
+
_state: PhantomData<State>,
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
impl Input<Unvalidated> {
|
|
1543
|
+
fn validate(self) -> Result<Input<Validated>, ValidationError> {
|
|
1544
|
+
// validation logic...
|
|
1545
|
+
Ok(Input { data: self.data, _state: PhantomData })
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
|
|
1549
|
+
impl Input<Validated> {
|
|
1550
|
+
fn process(&self) -> Output {
|
|
1551
|
+
// Only validated inputs can be processed
|
|
1552
|
+
}
|
|
1553
|
+
}
|
|
1554
|
+
```
|
|
1555
|
+
|
|
1556
|
+
### Don't
|
|
1557
|
+
|
|
1558
|
+
(Don't) Use raw primitive types for domain concepts.
|
|
1559
|
+
```rust
|
|
1560
|
+
// Bad: Raw strings lose semantic meaning
|
|
1561
|
+
fn load_package(name: String, path: String, uri: String) {
|
|
1562
|
+
// Easy to mix up arguments - compiler can't help
|
|
1563
|
+
}
|
|
1564
|
+
|
|
1565
|
+
// Bad: Easy to accidentally swap arguments
|
|
1566
|
+
load_package(uri, name, path); // Compiles but wrong!
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
(Don't) Create newtypes without validation when invariants exist.
|
|
1570
|
+
```rust
|
|
1571
|
+
// Bad: Allows invalid state
|
|
1572
|
+
pub struct Email(pub String); // pub field allows any string
|
|
1573
|
+
|
|
1574
|
+
// Better: Enforce invariants at construction
|
|
1575
|
+
pub struct Email(String); // Private field
|
|
1576
|
+
impl Email {
|
|
1577
|
+
pub fn new(s: &str) -> Result<Self, EmailError> {
|
|
1578
|
+
if s.contains('@') && s.contains('.') {
|
|
1579
|
+
Ok(Self(s.to_string()))
|
|
1580
|
+
} else {
|
|
1581
|
+
Err(EmailError::Invalid)
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
```
|
|
1586
|
+
|
|
1587
|
+
(Don't) Over-wrap types that don't need semantic distinction.
|
|
1588
|
+
```rust
|
|
1589
|
+
// Bad: Unnecessary wrapping of implementation details
|
|
1590
|
+
struct LoopCounter(usize); // Just use usize
|
|
1591
|
+
struct TempBuffer(Vec<u8>); // Just use Vec<u8>
|
|
1592
|
+
```
|
|
1593
|
+
|
|
1594
|
+
---
|
|
1595
|
+
|
|
1596
|
+
## rust/validation/typestate-guards
|
|
1597
|
+
|
|
1598
|
+
Use constructor validation to ensure only valid states can exist. Follow the 'parse, don't validate' principle: transform unvalidated data into validated types at system boundaries, making invalid states unrepresentable.
|
|
1599
|
+
|
|
1600
|
+
**Ratings:**
|
|
1601
|
+
- Impact: S (invalid states become unrepresentable)
|
|
1602
|
+
- Feasibility: B (requires upfront design thinking)
|
|
1603
|
+
- Idiomaticity: A (recommended Rust pattern)
|
|
1604
|
+
- FP Purity: A (Haskell-inspired 'make illegal states unrepresentable')
|
|
1605
|
+
|
|
1606
|
+
### Do
|
|
1607
|
+
|
|
1608
|
+
(Do) Validate in constructors to ensure type invariants.
|
|
1609
|
+
```rust
|
|
1610
|
+
impl Manifest {
|
|
1611
|
+
pub fn from_path(path: &Path) -> Result<Self, ManifestError> {
|
|
1612
|
+
let content = fs::read_to_string(path)
|
|
1613
|
+
.map_err(|e| ManifestError::ReadError { path: path.into(), source: e })?;
|
|
1614
|
+
Self::from_str(&content)
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
pub fn from_str(content: &str) -> Result<Self, ManifestError> {
|
|
1618
|
+
let manifest: Manifest = toml::from_str(content)
|
|
1619
|
+
.map_err(|e| ManifestError::ParseError(e.to_string()))?;
|
|
1620
|
+
manifest.validate()?; // Validation guards construction
|
|
1621
|
+
Ok(manifest)
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
fn validate(&self) -> Result<(), ManifestError> {
|
|
1625
|
+
match (&self.workspace, &self.package) {
|
|
1626
|
+
(Some(_), Some(_)) => Err(ManifestError::BothWorkspaceAndPackage),
|
|
1627
|
+
(None, None) => Err(ManifestError::NeitherWorkspaceNorPackage),
|
|
1628
|
+
_ => Ok(()),
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
(Do) Use the typestate pattern for compile-time state enforcement.
|
|
1635
|
+
```rust
|
|
1636
|
+
// States as zero-sized types
|
|
1637
|
+
struct Draft;
|
|
1638
|
+
struct Published;
|
|
1639
|
+
struct Archived;
|
|
1640
|
+
|
|
1641
|
+
struct Article<State> {
|
|
1642
|
+
id: ArticleId,
|
|
1643
|
+
title: String,
|
|
1644
|
+
content: String,
|
|
1645
|
+
_state: PhantomData<State>,
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
impl Article<Draft> {
|
|
1649
|
+
fn new(title: String, content: String) -> Self {
|
|
1650
|
+
Self { id: ArticleId::new(), title, content, _state: PhantomData }
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
fn publish(self) -> Article<Published> {
|
|
1654
|
+
Article { id: self.id, title: self.title, content: self.content, _state: PhantomData }
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1657
|
+
|
|
1658
|
+
impl Article<Published> {
|
|
1659
|
+
fn archive(self) -> Article<Archived> {
|
|
1660
|
+
Article { id: self.id, title: self.title, content: self.content, _state: PhantomData }
|
|
1661
|
+
}
|
|
1662
|
+
|
|
1663
|
+
fn view(&self) -> &str {
|
|
1664
|
+
&self.content // Only published articles can be viewed
|
|
1665
|
+
}
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
// Compile-time enforcement:
|
|
1669
|
+
// article_draft.view(); // Error: method not found
|
|
1670
|
+
// article_published.publish(); // Error: method not found
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
(Do) Parse into validated types at system boundaries.
|
|
1674
|
+
```rust
|
|
1675
|
+
// Raw input from external source
|
|
1676
|
+
struct RawUserInput {
|
|
1677
|
+
email: String,
|
|
1678
|
+
age: String,
|
|
1679
|
+
}
|
|
1680
|
+
|
|
1681
|
+
// Validated domain types
|
|
1682
|
+
struct Email(String);
|
|
1683
|
+
struct Age(u8);
|
|
1684
|
+
struct ValidatedUser {
|
|
1685
|
+
email: Email,
|
|
1686
|
+
age: Age,
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
impl RawUserInput {
|
|
1690
|
+
fn parse(self) -> Result<ValidatedUser, ValidationError> {
|
|
1691
|
+
let email = Email::parse(&self.email)?;
|
|
1692
|
+
let age = Age::parse(&self.age)?;
|
|
1693
|
+
Ok(ValidatedUser { email, age })
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
impl Email {
|
|
1698
|
+
fn parse(s: &str) -> Result<Self, ValidationError> {
|
|
1699
|
+
if s.contains('@') && s.len() > 3 {
|
|
1700
|
+
Ok(Self(s.to_string()))
|
|
1701
|
+
} else {
|
|
1702
|
+
Err(ValidationError::InvalidEmail(s.to_string()))
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
}
|
|
1706
|
+
```
|
|
1707
|
+
|
|
1708
|
+
(Do) Make the validated state obvious in function signatures.
|
|
1709
|
+
```rust
|
|
1710
|
+
// Functions that require validation communicate it via types
|
|
1711
|
+
fn send_email(to: &Email, subject: &str, body: &str) -> Result<(), SendError> {
|
|
1712
|
+
// Email is already validated - no need to check again
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
fn create_account(user: ValidatedUser) -> Result<Account, AccountError> {
|
|
1716
|
+
// All fields are pre-validated
|
|
1717
|
+
}
|
|
1718
|
+
```
|
|
1719
|
+
|
|
1720
|
+
### Don't
|
|
1721
|
+
|
|
1722
|
+
(Don't) Scatter validation logic throughout the codebase.
|
|
1723
|
+
```rust
|
|
1724
|
+
// Bad: Validation repeated everywhere
|
|
1725
|
+
fn send_email(to: &str, subject: &str, body: &str) -> Result<(), Error> {
|
|
1726
|
+
if !to.contains('@') {
|
|
1727
|
+
return Err(Error::InvalidEmail); // Validation here
|
|
1728
|
+
}
|
|
1729
|
+
// ... send logic
|
|
1730
|
+
}
|
|
1731
|
+
|
|
1732
|
+
fn save_user(email: &str) -> Result<(), Error> {
|
|
1733
|
+
if !email.contains('@') {
|
|
1734
|
+
return Err(Error::InvalidEmail); // Duplicate validation!
|
|
1735
|
+
}
|
|
1736
|
+
// ... save logic
|
|
1737
|
+
}
|
|
1738
|
+
```
|
|
1739
|
+
|
|
1740
|
+
(Don't) Allow construction of invalid objects.
|
|
1741
|
+
```rust
|
|
1742
|
+
// Bad: Public fields allow invalid state
|
|
1743
|
+
pub struct Email {
|
|
1744
|
+
pub address: String, // Anyone can set invalid value
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
// Bad: No validation in constructor
|
|
1748
|
+
impl User {
|
|
1749
|
+
pub fn new(email: String, age: i32) -> Self {
|
|
1750
|
+
Self { email, age } // Could be invalid!
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1753
|
+
```
|
|
1754
|
+
|
|
1755
|
+
(Don't) Use validation functions that return bool.
|
|
1756
|
+
```rust
|
|
1757
|
+
// Bad: Caller can ignore the result
|
|
1758
|
+
fn is_valid_email(s: &str) -> bool {
|
|
1759
|
+
s.contains('@')
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
let email = user_input;
|
|
1763
|
+
if is_valid_email(&email) {
|
|
1764
|
+
// What if we forget this check elsewhere?
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// Good: Parsing forces handling
|
|
1768
|
+
fn parse_email(s: &str) -> Result<Email, ValidationError>
|
|
1769
|
+
// Caller must handle the Result
|
|
1770
|
+
```
|
|
1771
|
+
|
|
1772
|
+
(Don't) Re-validate already-validated data.
|
|
1773
|
+
```rust
|
|
1774
|
+
// Bad: Redundant validation
|
|
1775
|
+
fn process(user: ValidatedUser) -> Result<(), Error> {
|
|
1776
|
+
// ValidatedUser is already valid by construction!
|
|
1777
|
+
if user.email.as_str().is_empty() {
|
|
1778
|
+
return Err(Error::InvalidEmail); // Unnecessary!
|
|
1779
|
+
}
|
|
1780
|
+
// ...
|
|
1781
|
+
}
|
|
1782
|
+
```
|
|
1783
|
+
|
|
1784
|
+
---
|