@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/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
+ ---