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