@canonical/code-standards 0.1.0 → 0.1.2
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/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- package/.github/workflows/ci.yml +40 -0
- package/biome.json +6 -0
- package/bun.lock +200 -0
- package/data/code.ttl +208 -167
- package/data/css.ttl +110 -91
- package/data/icons.ttl +186 -150
- package/data/packaging.ttl +428 -170
- package/data/react.ttl +306 -244
- package/data/rust.ttl +563 -467
- package/data/storybook.ttl +108 -90
- package/data/styling.ttl +40 -40
- package/data/tsdoc.ttl +111 -86
- package/data/turtle.ttl +89 -68
- package/definitions/CodeStandard.ttl +28 -20
- package/docs/code.md +37 -327
- package/docs/css.md +24 -20
- package/docs/icons.md +41 -42
- package/docs/index.md +2 -1
- package/docs/packaging.md +643 -0
- package/docs/react.md +58 -59
- package/docs/rust.md +92 -158
- package/docs/storybook.md +18 -20
- package/docs/styling.md +8 -8
- package/docs/tsdoc.md +16 -16
- package/docs/turtle.md +15 -15
- package/package.json +16 -2
- package/skills/add-standard/SKILL.md +83 -47
- package/src/scripts/generate-docs.ts +95 -13
- package/src/scripts/index.ts +4 -2
- package/tsconfig.json +8 -0
package/docs/rust.md
CHANGED
|
@@ -6,15 +6,9 @@ Standards for rust development.
|
|
|
6
6
|
|
|
7
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
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
9
|
### Do
|
|
16
10
|
|
|
17
|
-
|
|
11
|
+
Use the ? operator for clean Result propagation.
|
|
18
12
|
```rust
|
|
19
13
|
fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
|
|
20
14
|
let project_root = find_project_root()
|
|
@@ -38,7 +32,7 @@ fn load_project_graph() -> Result<(SemStore, Vec<PackageGraph>), GraphError> {
|
|
|
38
32
|
}
|
|
39
33
|
```
|
|
40
34
|
|
|
41
|
-
|
|
35
|
+
Use and_then for dependent computations.
|
|
42
36
|
```rust
|
|
43
37
|
fn process_user_input(input: &str) -> Result<Output, ProcessError> {
|
|
44
38
|
parse_input(input)
|
|
@@ -48,7 +42,7 @@ fn process_user_input(input: &str) -> Result<Output, ProcessError> {
|
|
|
48
42
|
}
|
|
49
43
|
```
|
|
50
44
|
|
|
51
|
-
|
|
45
|
+
Use map for infallible transformations within Result.
|
|
52
46
|
```rust
|
|
53
47
|
fn get_package_names(path: &Path) -> Result<Vec<String>, IoError> {
|
|
54
48
|
fs::read_dir(path)?
|
|
@@ -67,7 +61,7 @@ fn get_config_value(key: &str) -> Result<String, ConfigError> {
|
|
|
67
61
|
}
|
|
68
62
|
```
|
|
69
63
|
|
|
70
|
-
|
|
64
|
+
Use or_else for fallback computations.
|
|
71
65
|
```rust
|
|
72
66
|
fn find_config() -> Result<Config, ConfigError> {
|
|
73
67
|
load_config_from_env()
|
|
@@ -76,7 +70,7 @@ fn find_config() -> Result<Config, ConfigError> {
|
|
|
76
70
|
}
|
|
77
71
|
```
|
|
78
72
|
|
|
79
|
-
|
|
73
|
+
Combine multiple Results with the ? operator in sequence.
|
|
80
74
|
```rust
|
|
81
75
|
fn setup_application() -> Result<App, SetupError> {
|
|
82
76
|
let config = load_config()?;
|
|
@@ -90,7 +84,7 @@ fn setup_application() -> Result<App, SetupError> {
|
|
|
90
84
|
|
|
91
85
|
### Don't
|
|
92
86
|
|
|
93
|
-
|
|
87
|
+
Use nested match expressions for Result handling.
|
|
94
88
|
```rust
|
|
95
89
|
// Bad: Deeply nested, hard to follow
|
|
96
90
|
fn process(input: &str) -> Result<Output, Error> {
|
|
@@ -111,7 +105,7 @@ fn process(input: &str) -> Result<Output, Error> {
|
|
|
111
105
|
}
|
|
112
106
|
```
|
|
113
107
|
|
|
114
|
-
|
|
108
|
+
Use unwrap() or expect() to bypass error handling.
|
|
115
109
|
```rust
|
|
116
110
|
// Bad: Panics on error
|
|
117
111
|
fn load_data() -> Data {
|
|
@@ -120,7 +114,7 @@ fn load_data() -> Data {
|
|
|
120
114
|
}
|
|
121
115
|
```
|
|
122
116
|
|
|
123
|
-
|
|
117
|
+
Ignore errors silently.
|
|
124
118
|
```rust
|
|
125
119
|
// Bad: Errors are silently dropped
|
|
126
120
|
fn try_save(data: &Data) {
|
|
@@ -128,7 +122,7 @@ fn try_save(data: &Data) {
|
|
|
128
122
|
}
|
|
129
123
|
```
|
|
130
124
|
|
|
131
|
-
|
|
125
|
+
Convert all errors to strings early in the chain.
|
|
132
126
|
```rust
|
|
133
127
|
// Bad: Loses error type information
|
|
134
128
|
fn process() -> Result<(), String> {
|
|
@@ -144,15 +138,9 @@ fn process() -> Result<(), String> {
|
|
|
144
138
|
|
|
145
139
|
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
140
|
|
|
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
141
|
### Do
|
|
154
142
|
|
|
155
|
-
|
|
143
|
+
Use thiserror to create structured, contextual error types.
|
|
156
144
|
```rust
|
|
157
145
|
use thiserror::Error;
|
|
158
146
|
use std::path::PathBuf;
|
|
@@ -183,7 +171,7 @@ pub enum GraphError {
|
|
|
183
171
|
}
|
|
184
172
|
```
|
|
185
173
|
|
|
186
|
-
|
|
174
|
+
Use map_err to add context when propagating errors.
|
|
187
175
|
```rust
|
|
188
176
|
fn load_package(path: &Path) -> Result<Package, GraphError> {
|
|
189
177
|
let content = fs::read_to_string(path)
|
|
@@ -202,7 +190,7 @@ fn load_package(path: &Path) -> Result<Package, GraphError> {
|
|
|
202
190
|
}
|
|
203
191
|
```
|
|
204
192
|
|
|
205
|
-
|
|
193
|
+
Chain errors to preserve the full error trail.
|
|
206
194
|
```rust
|
|
207
195
|
#[derive(Error, Debug)]
|
|
208
196
|
pub enum AppError {
|
|
@@ -221,7 +209,7 @@ pub enum AppError {
|
|
|
221
209
|
}
|
|
222
210
|
```
|
|
223
211
|
|
|
224
|
-
|
|
212
|
+
Include actionable information in error messages.
|
|
225
213
|
```rust
|
|
226
214
|
#[derive(Error, Debug)]
|
|
227
215
|
pub enum ValidationError {
|
|
@@ -235,7 +223,7 @@ pub enum ValidationError {
|
|
|
235
223
|
|
|
236
224
|
### Don't
|
|
237
225
|
|
|
238
|
-
|
|
226
|
+
Use generic string errors that lose context.
|
|
239
227
|
```rust
|
|
240
228
|
// Bad: Loses type information and context
|
|
241
229
|
fn load_config(path: &Path) -> Result<Config, String> {
|
|
@@ -247,7 +235,7 @@ fn load_config(path: &Path) -> Result<Config, String> {
|
|
|
247
235
|
}
|
|
248
236
|
```
|
|
249
237
|
|
|
250
|
-
|
|
238
|
+
Discard error information with unwrap or expect in library code.
|
|
251
239
|
```rust
|
|
252
240
|
// Bad: Panics instead of propagating
|
|
253
241
|
fn parse_uri(s: &str) -> Uri {
|
|
@@ -258,7 +246,7 @@ fn parse_uri(s: &str) -> Uri {
|
|
|
258
246
|
let file = File::open(path).expect("failed"); // Which path?
|
|
259
247
|
```
|
|
260
248
|
|
|
261
|
-
|
|
249
|
+
Create error types without Display or Debug.
|
|
262
250
|
```rust
|
|
263
251
|
// Bad: Unusable error type
|
|
264
252
|
pub struct MyError {
|
|
@@ -267,7 +255,7 @@ pub struct MyError {
|
|
|
267
255
|
}
|
|
268
256
|
```
|
|
269
257
|
|
|
270
|
-
|
|
258
|
+
Use anyhow/eyre in library code (ok for applications).
|
|
271
259
|
```rust
|
|
272
260
|
// Bad in libraries: Erases type information
|
|
273
261
|
pub fn process() -> anyhow::Result<()> {
|
|
@@ -286,15 +274,9 @@ pub fn process() -> Result<(), ProcessError> {
|
|
|
286
274
|
|
|
287
275
|
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
276
|
|
|
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
277
|
### Do
|
|
296
278
|
|
|
297
|
-
|
|
279
|
+
Design functions as Kleisli arrows: `A -> Result<B, E>`.
|
|
298
280
|
```rust
|
|
299
281
|
// Each function is a Kleisli arrow that can be composed
|
|
300
282
|
fn parse_config(input: &str) -> Result<RawConfig, ParseError> { /* ... */ }
|
|
@@ -320,7 +302,7 @@ fn process_input(s: &str) -> Result<Output, ProcessError> {
|
|
|
320
302
|
}
|
|
321
303
|
```
|
|
322
304
|
|
|
323
|
-
|
|
305
|
+
Use combinators to compose fallible operations.
|
|
324
306
|
```rust
|
|
325
307
|
impl DepSpec {
|
|
326
308
|
pub fn from_value(value: &DependencyValue) -> Result<Self, ParseError> {
|
|
@@ -340,7 +322,7 @@ impl DepSpec {
|
|
|
340
322
|
}
|
|
341
323
|
```
|
|
342
324
|
|
|
343
|
-
|
|
325
|
+
Create helper traits for method chaining when needed.
|
|
344
326
|
```rust
|
|
345
327
|
trait ResultExt<T, E> {
|
|
346
328
|
fn and_try<U, F>(self, f: F) -> Result<U, E>
|
|
@@ -364,7 +346,7 @@ let result = input
|
|
|
364
346
|
.and_try(step3);
|
|
365
347
|
```
|
|
366
348
|
|
|
367
|
-
|
|
349
|
+
Use the pipe pattern for readability.
|
|
368
350
|
```rust
|
|
369
351
|
// With a pipe trait or tap crate
|
|
370
352
|
trait Pipe: Sized {
|
|
@@ -383,7 +365,7 @@ let result = input
|
|
|
383
365
|
|
|
384
366
|
### Don't
|
|
385
367
|
|
|
386
|
-
|
|
368
|
+
Mix side effects into pure transformation chains.
|
|
387
369
|
```rust
|
|
388
370
|
// Bad: Side effects hidden in chain
|
|
389
371
|
fn process(input: &str) -> Result<Output, Error> {
|
|
@@ -406,7 +388,7 @@ fn process(input: &str) -> Result<Output, Error> {
|
|
|
406
388
|
}
|
|
407
389
|
```
|
|
408
390
|
|
|
409
|
-
|
|
391
|
+
Break the chain with early returns when and_then works.
|
|
410
392
|
```rust
|
|
411
393
|
// Bad: Breaks the monadic flow
|
|
412
394
|
fn process(input: &str) -> Result<Output, Error> {
|
|
@@ -431,7 +413,7 @@ fn process(input: &str) -> Result<Output, Error> {
|
|
|
431
413
|
}
|
|
432
414
|
```
|
|
433
415
|
|
|
434
|
-
|
|
416
|
+
Use unwrap in the middle of a chain.
|
|
435
417
|
```rust
|
|
436
418
|
// Bad: Panics break the monadic abstraction
|
|
437
419
|
let result = items.iter()
|
|
@@ -446,15 +428,9 @@ let result = items.iter()
|
|
|
446
428
|
|
|
447
429
|
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
430
|
|
|
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
431
|
### Do
|
|
456
432
|
|
|
457
|
-
|
|
433
|
+
Use iterator chains for data transformations.
|
|
458
434
|
```rust
|
|
459
435
|
fn get_installed_packages() -> Result<Vec<String>, IoError> {
|
|
460
436
|
let packages_path = sem_packages_dir()?;
|
|
@@ -478,7 +454,7 @@ fn get_installed_packages() -> Result<Vec<String>, IoError> {
|
|
|
478
454
|
}
|
|
479
455
|
```
|
|
480
456
|
|
|
481
|
-
|
|
457
|
+
Use flat_map for one-to-many transformations.
|
|
482
458
|
```rust
|
|
483
459
|
fn get_all_dependencies(packages: &[Package]) -> Vec<Dependency> {
|
|
484
460
|
packages.iter()
|
|
@@ -487,7 +463,7 @@ fn get_all_dependencies(packages: &[Package]) -> Vec<Dependency> {
|
|
|
487
463
|
}
|
|
488
464
|
```
|
|
489
465
|
|
|
490
|
-
|
|
466
|
+
Use filter_map to combine filter and map operations.
|
|
491
467
|
```rust
|
|
492
468
|
fn parse_valid_numbers(strings: &[&str]) -> Vec<i32> {
|
|
493
469
|
strings.iter()
|
|
@@ -496,7 +472,7 @@ fn parse_valid_numbers(strings: &[&str]) -> Vec<i32> {
|
|
|
496
472
|
}
|
|
497
473
|
```
|
|
498
474
|
|
|
499
|
-
|
|
475
|
+
Use fold/reduce for accumulating results.
|
|
500
476
|
```rust
|
|
501
477
|
fn total_size(files: &[PathBuf]) -> u64 {
|
|
502
478
|
files.iter()
|
|
@@ -511,7 +487,7 @@ fn merge_configs(configs: &[Config]) -> Config {
|
|
|
511
487
|
}
|
|
512
488
|
```
|
|
513
489
|
|
|
514
|
-
|
|
490
|
+
Chain multiple operations for complex transformations.
|
|
515
491
|
```rust
|
|
516
492
|
fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>> {
|
|
517
493
|
entries.iter()
|
|
@@ -526,7 +502,7 @@ fn process_log_entries(entries: &[LogEntry]) -> HashMap<String, Vec<&LogEntry>>
|
|
|
526
502
|
}
|
|
527
503
|
```
|
|
528
504
|
|
|
529
|
-
|
|
505
|
+
Use collect with turbofish for type-driven collection.
|
|
530
506
|
```rust
|
|
531
507
|
// Collect into different types based on need
|
|
532
508
|
let vec: Vec<_> = iter.collect();
|
|
@@ -541,7 +517,7 @@ let results: Result<Vec<_>, _> = items.iter()
|
|
|
541
517
|
|
|
542
518
|
### Don't
|
|
543
519
|
|
|
544
|
-
|
|
520
|
+
Use imperative loops when combinators are clearer.
|
|
545
521
|
```rust
|
|
546
522
|
// Bad: Imperative style obscures intent
|
|
547
523
|
fn get_names(users: &[User]) -> Vec<String> {
|
|
@@ -563,7 +539,7 @@ fn get_names(users: &[User]) -> Vec<String> {
|
|
|
563
539
|
}
|
|
564
540
|
```
|
|
565
541
|
|
|
566
|
-
|
|
542
|
+
Collect intermediate results unnecessarily.
|
|
567
543
|
```rust
|
|
568
544
|
// Bad: Unnecessary allocation
|
|
569
545
|
let filtered: Vec<_> = items.iter().filter(|x| x.valid).collect();
|
|
@@ -577,7 +553,7 @@ let result: i32 = items.iter()
|
|
|
577
553
|
.sum();
|
|
578
554
|
```
|
|
579
555
|
|
|
580
|
-
|
|
556
|
+
Use for loops just to build up a Vec.
|
|
581
557
|
```rust
|
|
582
558
|
// Bad: Manual Vec building
|
|
583
559
|
let mut results = Vec::new();
|
|
@@ -589,7 +565,7 @@ for item in items {
|
|
|
589
565
|
let results: Vec<_> = items.iter().map(transform).collect();
|
|
590
566
|
```
|
|
591
567
|
|
|
592
|
-
|
|
568
|
+
Nest loops when flat_map works.
|
|
593
569
|
```rust
|
|
594
570
|
// Bad: Nested loops
|
|
595
571
|
let mut all_items = Vec::new();
|
|
@@ -611,15 +587,9 @@ let all_items: Vec<_> = containers.iter()
|
|
|
611
587
|
|
|
612
588
|
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
589
|
|
|
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
590
|
### Do
|
|
621
591
|
|
|
622
|
-
|
|
592
|
+
Use Option combinators for clean transformations.
|
|
623
593
|
```rust
|
|
624
594
|
impl Manifest {
|
|
625
595
|
pub fn display_name(&self) -> Option<String> {
|
|
@@ -638,7 +608,7 @@ impl Manifest {
|
|
|
638
608
|
}
|
|
639
609
|
```
|
|
640
610
|
|
|
641
|
-
|
|
611
|
+
Use unwrap_or and unwrap_or_else for defaults.
|
|
642
612
|
```rust
|
|
643
613
|
fn get_config_value(key: &str) -> String {
|
|
644
614
|
config.get(key)
|
|
@@ -654,7 +624,7 @@ fn get_timeout() -> Duration {
|
|
|
654
624
|
}
|
|
655
625
|
```
|
|
656
626
|
|
|
657
|
-
|
|
627
|
+
Use ok_or to convert Option to Result.
|
|
658
628
|
```rust
|
|
659
629
|
fn find_package(name: &str) -> Result<&Package, PackageError> {
|
|
660
630
|
packages.get(name)
|
|
@@ -667,7 +637,7 @@ fn get_required_field<'a>(map: &'a HashMap<String, Value>, key: &str) -> Result<
|
|
|
667
637
|
}
|
|
668
638
|
```
|
|
669
639
|
|
|
670
|
-
|
|
640
|
+
Use filter and filter_map for conditional processing.
|
|
671
641
|
```rust
|
|
672
642
|
fn find_active_user(id: UserId) -> Option<User> {
|
|
673
643
|
users.get(&id).filter(|u| u.is_active)
|
|
@@ -680,7 +650,7 @@ fn get_valid_entries(entries: &[Entry]) -> Vec<&Entry> {
|
|
|
680
650
|
}
|
|
681
651
|
```
|
|
682
652
|
|
|
683
|
-
|
|
653
|
+
Use the ? operator with Option in functions returning Option.
|
|
684
654
|
```rust
|
|
685
655
|
fn get_nested_value(data: &Data) -> Option<&str> {
|
|
686
656
|
let section = data.sections.get("main")?;
|
|
@@ -691,7 +661,7 @@ fn get_nested_value(data: &Data) -> Option<&str> {
|
|
|
691
661
|
|
|
692
662
|
### Don't
|
|
693
663
|
|
|
694
|
-
|
|
664
|
+
Use sentinel values instead of Option.
|
|
695
665
|
```rust
|
|
696
666
|
// Bad: Magic values
|
|
697
667
|
fn find_index(items: &[Item], target: &Item) -> i32 {
|
|
@@ -705,7 +675,7 @@ fn find_index(items: &[Item], target: &Item) -> Option<usize> {
|
|
|
705
675
|
}
|
|
706
676
|
```
|
|
707
677
|
|
|
708
|
-
|
|
678
|
+
Overuse unwrap() or expect() outside of tests.
|
|
709
679
|
```rust
|
|
710
680
|
// Bad: Panics on None
|
|
711
681
|
let user = users.get(id).unwrap();
|
|
@@ -716,7 +686,7 @@ let user = users.get(id).ok_or(UserError::NotFound(id))?;
|
|
|
716
686
|
let name = user.name.as_deref().unwrap_or("Anonymous");
|
|
717
687
|
```
|
|
718
688
|
|
|
719
|
-
|
|
689
|
+
Check is_some/is_none then unwrap.
|
|
720
690
|
```rust
|
|
721
691
|
// Bad: Redundant check
|
|
722
692
|
if value.is_some() {
|
|
@@ -733,7 +703,7 @@ if let Some(v) = value {
|
|
|
733
703
|
value.map(process);
|
|
734
704
|
```
|
|
735
705
|
|
|
736
|
-
|
|
706
|
+
Create deeply nested Option chains without combinators.
|
|
737
707
|
```rust
|
|
738
708
|
// Bad: Nested matching
|
|
739
709
|
match outer {
|
|
@@ -760,15 +730,9 @@ outer
|
|
|
760
730
|
|
|
761
731
|
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
732
|
|
|
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
733
|
### Do
|
|
770
734
|
|
|
771
|
-
|
|
735
|
+
Use proptest for property-based testing.
|
|
772
736
|
```rust
|
|
773
737
|
use proptest::prelude::*;
|
|
774
738
|
|
|
@@ -794,7 +758,7 @@ proptest! {
|
|
|
794
758
|
}
|
|
795
759
|
```
|
|
796
760
|
|
|
797
|
-
|
|
761
|
+
Test algebraic properties (identity, associativity, commutativity).
|
|
798
762
|
```rust
|
|
799
763
|
proptest! {
|
|
800
764
|
#[test]
|
|
@@ -814,7 +778,7 @@ proptest! {
|
|
|
814
778
|
}
|
|
815
779
|
```
|
|
816
780
|
|
|
817
|
-
|
|
781
|
+
Test invariants that should hold for all valid inputs.
|
|
818
782
|
```rust
|
|
819
783
|
proptest! {
|
|
820
784
|
#[test]
|
|
@@ -838,7 +802,7 @@ proptest! {
|
|
|
838
802
|
}
|
|
839
803
|
```
|
|
840
804
|
|
|
841
|
-
|
|
805
|
+
Use custom strategies for domain-specific types.
|
|
842
806
|
```rust
|
|
843
807
|
fn valid_package_name() -> impl Strategy<Value = String> {
|
|
844
808
|
"[a-z][a-z0-9-]{0,62}[a-z0-9]?"
|
|
@@ -860,7 +824,7 @@ proptest! {
|
|
|
860
824
|
|
|
861
825
|
### Don't
|
|
862
826
|
|
|
863
|
-
|
|
827
|
+
Only write example-based unit tests for complex logic.
|
|
864
828
|
```rust
|
|
865
829
|
// Incomplete: Only tests a few examples
|
|
866
830
|
#[test]
|
|
@@ -873,7 +837,7 @@ fn test_merge() {
|
|
|
873
837
|
}
|
|
874
838
|
```
|
|
875
839
|
|
|
876
|
-
|
|
840
|
+
Ignore test failures without understanding the counterexample.
|
|
877
841
|
```rust
|
|
878
842
|
// Bad: Ignoring failures
|
|
879
843
|
proptest! {
|
|
@@ -885,7 +849,7 @@ proptest! {
|
|
|
885
849
|
}
|
|
886
850
|
```
|
|
887
851
|
|
|
888
|
-
|
|
852
|
+
Write properties that are too weak or tautological.
|
|
889
853
|
```rust
|
|
890
854
|
// Bad: This always passes, tests nothing useful
|
|
891
855
|
proptest! {
|
|
@@ -905,7 +869,7 @@ proptest! {
|
|
|
905
869
|
}
|
|
906
870
|
```
|
|
907
871
|
|
|
908
|
-
|
|
872
|
+
Generate invalid inputs without proper filtering.
|
|
909
873
|
```rust
|
|
910
874
|
// Bad: Generates invalid UTF-8 and panics
|
|
911
875
|
proptest! {
|
|
@@ -930,15 +894,9 @@ proptest! {
|
|
|
930
894
|
|
|
931
895
|
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
896
|
|
|
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
897
|
### Do
|
|
940
898
|
|
|
941
|
-
|
|
899
|
+
Write the test first, watch it fail.
|
|
942
900
|
```rust
|
|
943
901
|
// Step 1: RED - Write failing test
|
|
944
902
|
#[cfg(test)]
|
|
@@ -957,7 +915,7 @@ mod tests {
|
|
|
957
915
|
// At this point: cargo test fails - Version doesn't exist yet!
|
|
958
916
|
```
|
|
959
917
|
|
|
960
|
-
|
|
918
|
+
Implement the minimum code to pass.
|
|
961
919
|
```rust
|
|
962
920
|
// Step 2: GREEN - Minimal implementation
|
|
963
921
|
pub struct Version {
|
|
@@ -983,7 +941,7 @@ impl Version {
|
|
|
983
941
|
// cargo test passes!
|
|
984
942
|
```
|
|
985
943
|
|
|
986
|
-
|
|
944
|
+
Refactor while keeping tests green.
|
|
987
945
|
```rust
|
|
988
946
|
// Step 3: REFACTOR - Improve design
|
|
989
947
|
impl Version {
|
|
@@ -1011,7 +969,7 @@ impl Version {
|
|
|
1011
969
|
// Tests still pass after refactoring!
|
|
1012
970
|
```
|
|
1013
971
|
|
|
1014
|
-
|
|
972
|
+
Add tests for edge cases incrementally.
|
|
1015
973
|
```rust
|
|
1016
974
|
#[test]
|
|
1017
975
|
fn parse_rejects_empty_string() {
|
|
@@ -1035,7 +993,7 @@ fn parse_handles_leading_zeros() {
|
|
|
1035
993
|
}
|
|
1036
994
|
```
|
|
1037
995
|
|
|
1038
|
-
|
|
996
|
+
Use test modules colocated with implementation.
|
|
1039
997
|
```rust
|
|
1040
998
|
// src/version.rs
|
|
1041
999
|
pub struct Version { /* ... */ }
|
|
@@ -1056,7 +1014,7 @@ mod tests {
|
|
|
1056
1014
|
|
|
1057
1015
|
### Don't
|
|
1058
1016
|
|
|
1059
|
-
|
|
1017
|
+
Write implementation before tests.
|
|
1060
1018
|
```rust
|
|
1061
1019
|
// Bad: Implementation without tests
|
|
1062
1020
|
pub fn complex_algorithm(input: &str) -> Result<Output, Error> {
|
|
@@ -1066,7 +1024,7 @@ pub fn complex_algorithm(input: &str) -> Result<Output, Error> {
|
|
|
1066
1024
|
}
|
|
1067
1025
|
```
|
|
1068
1026
|
|
|
1069
|
-
|
|
1027
|
+
Write tests that pass trivially or test nothing.
|
|
1070
1028
|
```rust
|
|
1071
1029
|
// Bad: Test that always passes
|
|
1072
1030
|
#[test]
|
|
@@ -1081,7 +1039,7 @@ fn test_without_assertions() {
|
|
|
1081
1039
|
}
|
|
1082
1040
|
```
|
|
1083
1041
|
|
|
1084
|
-
|
|
1042
|
+
Skip the refactor step.
|
|
1085
1043
|
```rust
|
|
1086
1044
|
// Bad: "It works, ship it!"
|
|
1087
1045
|
impl Version {
|
|
@@ -1099,7 +1057,7 @@ impl Version {
|
|
|
1099
1057
|
}
|
|
1100
1058
|
```
|
|
1101
1059
|
|
|
1102
|
-
|
|
1060
|
+
Write tests after the fact that just confirm current behavior.
|
|
1103
1061
|
```rust
|
|
1104
1062
|
// Bad: "Characterization tests" without understanding intent
|
|
1105
1063
|
#[test]
|
|
@@ -1109,7 +1067,7 @@ fn test_weird_behavior() {
|
|
|
1109
1067
|
}
|
|
1110
1068
|
```
|
|
1111
1069
|
|
|
1112
|
-
|
|
1070
|
+
Test private implementation details.
|
|
1113
1071
|
```rust
|
|
1114
1072
|
// Bad: Testing internals that may change
|
|
1115
1073
|
#[test]
|
|
@@ -1135,15 +1093,9 @@ fn test_caching_behavior() {
|
|
|
1135
1093
|
|
|
1136
1094
|
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
1095
|
|
|
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
1096
|
### Do
|
|
1145
1097
|
|
|
1146
|
-
|
|
1098
|
+
Design small, single-purpose traits.
|
|
1147
1099
|
```rust
|
|
1148
1100
|
/// Can be resolved from a prefixed form to a full URI
|
|
1149
1101
|
trait Resolvable {
|
|
@@ -1168,7 +1120,7 @@ trait Loadable: Sized {
|
|
|
1168
1120
|
}
|
|
1169
1121
|
```
|
|
1170
1122
|
|
|
1171
|
-
|
|
1123
|
+
Compose traits using supertraits and bounds.
|
|
1172
1124
|
```rust
|
|
1173
1125
|
// Compose small traits into larger capabilities
|
|
1174
1126
|
trait UriHandler: Resolvable + Compactable {}
|
|
@@ -1182,7 +1134,7 @@ fn process_uri<T: Resolvable + Display>(uri: &T, map: &PrefixMap) -> String {
|
|
|
1182
1134
|
}
|
|
1183
1135
|
```
|
|
1184
1136
|
|
|
1185
|
-
|
|
1137
|
+
Use extension traits to add methods to existing types.
|
|
1186
1138
|
```rust
|
|
1187
1139
|
trait ResultExt<T, E> {
|
|
1188
1140
|
fn context(self, msg: &str) -> Result<T, ContextError<E>>;
|
|
@@ -1204,7 +1156,7 @@ let data = fs::read_to_string(path)
|
|
|
1204
1156
|
.context("failed to read config")?;
|
|
1205
1157
|
```
|
|
1206
1158
|
|
|
1207
|
-
|
|
1159
|
+
Implement standard library traits for interoperability.
|
|
1208
1160
|
```rust
|
|
1209
1161
|
impl Default for SemStore {
|
|
1210
1162
|
fn default() -> Self {
|
|
@@ -1229,7 +1181,7 @@ impl FromStr for Version {
|
|
|
1229
1181
|
|
|
1230
1182
|
### Don't
|
|
1231
1183
|
|
|
1232
|
-
|
|
1184
|
+
Create large, monolithic traits.
|
|
1233
1185
|
```rust
|
|
1234
1186
|
// Bad: Too many responsibilities
|
|
1235
1187
|
trait Repository {
|
|
@@ -1247,7 +1199,7 @@ trait Repository {
|
|
|
1247
1199
|
}
|
|
1248
1200
|
```
|
|
1249
1201
|
|
|
1250
|
-
|
|
1202
|
+
Use trait objects when generics suffice.
|
|
1251
1203
|
```rust
|
|
1252
1204
|
// Bad: Unnecessary dynamic dispatch
|
|
1253
1205
|
fn process(items: &[Box<dyn Processable>]) {
|
|
@@ -1264,7 +1216,7 @@ fn process<T: Processable>(items: &[T]) {
|
|
|
1264
1216
|
}
|
|
1265
1217
|
```
|
|
1266
1218
|
|
|
1267
|
-
|
|
1219
|
+
Require unused trait methods via blanket requirements.
|
|
1268
1220
|
```rust
|
|
1269
1221
|
// Bad: Forces implementers to provide unused methods
|
|
1270
1222
|
trait DataStore: Connect + Query + Mutate + Transaction + Cache + Log {
|
|
@@ -1277,7 +1229,7 @@ fn process<T: Query + Mutate>(store: &mut T) {
|
|
|
1277
1229
|
}
|
|
1278
1230
|
```
|
|
1279
1231
|
|
|
1280
|
-
|
|
1232
|
+
Use associated types when generic parameters work better.
|
|
1281
1233
|
```rust
|
|
1282
1234
|
// Bad: Can only have one implementation per type
|
|
1283
1235
|
trait Container {
|
|
@@ -1298,15 +1250,9 @@ trait Container<T> {
|
|
|
1298
1250
|
|
|
1299
1251
|
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
1252
|
|
|
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
1253
|
### Do
|
|
1308
1254
|
|
|
1309
|
-
|
|
1255
|
+
Use enums to model mutually exclusive states.
|
|
1310
1256
|
```rust
|
|
1311
1257
|
#[derive(Debug, Clone, PartialEq)]
|
|
1312
1258
|
pub enum DepSpec {
|
|
@@ -1333,7 +1279,7 @@ impl DepSpec {
|
|
|
1333
1279
|
}
|
|
1334
1280
|
```
|
|
1335
1281
|
|
|
1336
|
-
|
|
1282
|
+
Use enums for state machines with compile-time guarantees.
|
|
1337
1283
|
```rust
|
|
1338
1284
|
enum ConnectionState {
|
|
1339
1285
|
Disconnected,
|
|
@@ -1363,7 +1309,7 @@ impl ConnectionState {
|
|
|
1363
1309
|
}
|
|
1364
1310
|
```
|
|
1365
1311
|
|
|
1366
|
-
|
|
1312
|
+
Use enums to make invalid states unrepresentable.
|
|
1367
1313
|
```rust
|
|
1368
1314
|
// User can be either anonymous or authenticated, never both
|
|
1369
1315
|
enum User {
|
|
@@ -1383,7 +1329,7 @@ enum FieldState<T> {
|
|
|
1383
1329
|
}
|
|
1384
1330
|
```
|
|
1385
1331
|
|
|
1386
|
-
|
|
1332
|
+
Leverage exhaustive matching for safety.
|
|
1387
1333
|
```rust
|
|
1388
1334
|
fn handle_result(result: QueryResult) -> Response {
|
|
1389
1335
|
match result {
|
|
@@ -1400,7 +1346,7 @@ fn handle_result(result: QueryResult) -> Response {
|
|
|
1400
1346
|
|
|
1401
1347
|
### Don't
|
|
1402
1348
|
|
|
1403
|
-
|
|
1349
|
+
Use boolean flags for mutually exclusive states.
|
|
1404
1350
|
```rust
|
|
1405
1351
|
// Bad: Multiple bools can have invalid combinations
|
|
1406
1352
|
struct User {
|
|
@@ -1415,7 +1361,7 @@ struct Connection {
|
|
|
1415
1361
|
}
|
|
1416
1362
|
```
|
|
1417
1363
|
|
|
1418
|
-
|
|
1364
|
+
Use Option when you need more than two states.
|
|
1419
1365
|
```rust
|
|
1420
1366
|
// Bad: Option doesn't capture "loading" vs "error" vs "empty"
|
|
1421
1367
|
struct DataView {
|
|
@@ -1431,7 +1377,7 @@ enum DataState {
|
|
|
1431
1377
|
}
|
|
1432
1378
|
```
|
|
1433
1379
|
|
|
1434
|
-
|
|
1380
|
+
Use inheritance-like patterns with trait objects when enums suffice.
|
|
1435
1381
|
```rust
|
|
1436
1382
|
// Bad: Runtime dispatch when compile-time would work
|
|
1437
1383
|
trait Shape {
|
|
@@ -1456,7 +1402,7 @@ impl Shape {
|
|
|
1456
1402
|
}
|
|
1457
1403
|
```
|
|
1458
1404
|
|
|
1459
|
-
|
|
1405
|
+
Use integers or strings as type discriminators.
|
|
1460
1406
|
```rust
|
|
1461
1407
|
// Bad: Magic numbers
|
|
1462
1408
|
const USER_TYPE_ADMIN: i32 = 1;
|
|
@@ -1473,15 +1419,9 @@ struct Message { msg_type: String } // "request", "response", typos!
|
|
|
1473
1419
|
|
|
1474
1420
|
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
1421
|
|
|
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
1422
|
### Do
|
|
1483
1423
|
|
|
1484
|
-
|
|
1424
|
+
Wrap primitive types in newtypes for domain semantics.
|
|
1485
1425
|
```rust
|
|
1486
1426
|
/// A validated package name following sem conventions
|
|
1487
1427
|
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
|
@@ -1505,7 +1445,7 @@ impl PackageName {
|
|
|
1505
1445
|
}
|
|
1506
1446
|
```
|
|
1507
1447
|
|
|
1508
|
-
|
|
1448
|
+
Use newtypes to distinguish semantically different IDs.
|
|
1509
1449
|
```rust
|
|
1510
1450
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
1511
1451
|
pub struct UserId(u64);
|
|
@@ -1520,14 +1460,14 @@ fn process_order(user: UserId, order: OrderId) { /* ... */ }
|
|
|
1520
1460
|
// process_order(order_id, user_id); // Error: expected UserId, found OrderId
|
|
1521
1461
|
```
|
|
1522
1462
|
|
|
1523
|
-
|
|
1463
|
+
Derive common traits to maintain ergonomics.
|
|
1524
1464
|
```rust
|
|
1525
1465
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
1526
1466
|
#[serde(transparent)]
|
|
1527
1467
|
pub struct Uri(String);
|
|
1528
1468
|
```
|
|
1529
1469
|
|
|
1530
|
-
|
|
1470
|
+
Use phantom types for compile-time state tracking.
|
|
1531
1471
|
```rust
|
|
1532
1472
|
use std::marker::PhantomData;
|
|
1533
1473
|
|
|
@@ -1555,7 +1495,7 @@ impl Input<Validated> {
|
|
|
1555
1495
|
|
|
1556
1496
|
### Don't
|
|
1557
1497
|
|
|
1558
|
-
|
|
1498
|
+
Use raw primitive types for domain concepts.
|
|
1559
1499
|
```rust
|
|
1560
1500
|
// Bad: Raw strings lose semantic meaning
|
|
1561
1501
|
fn load_package(name: String, path: String, uri: String) {
|
|
@@ -1566,7 +1506,7 @@ fn load_package(name: String, path: String, uri: String) {
|
|
|
1566
1506
|
load_package(uri, name, path); // Compiles but wrong!
|
|
1567
1507
|
```
|
|
1568
1508
|
|
|
1569
|
-
|
|
1509
|
+
Create newtypes without validation when invariants exist.
|
|
1570
1510
|
```rust
|
|
1571
1511
|
// Bad: Allows invalid state
|
|
1572
1512
|
pub struct Email(pub String); // pub field allows any string
|
|
@@ -1584,7 +1524,7 @@ impl Email {
|
|
|
1584
1524
|
}
|
|
1585
1525
|
```
|
|
1586
1526
|
|
|
1587
|
-
|
|
1527
|
+
Over-wrap types that don't need semantic distinction.
|
|
1588
1528
|
```rust
|
|
1589
1529
|
// Bad: Unnecessary wrapping of implementation details
|
|
1590
1530
|
struct LoopCounter(usize); // Just use usize
|
|
@@ -1597,15 +1537,9 @@ struct TempBuffer(Vec<u8>); // Just use Vec<u8>
|
|
|
1597
1537
|
|
|
1598
1538
|
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
1539
|
|
|
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
1540
|
### Do
|
|
1607
1541
|
|
|
1608
|
-
|
|
1542
|
+
Validate in constructors to ensure type invariants.
|
|
1609
1543
|
```rust
|
|
1610
1544
|
impl Manifest {
|
|
1611
1545
|
pub fn from_path(path: &Path) -> Result<Self, ManifestError> {
|
|
@@ -1631,7 +1565,7 @@ impl Manifest {
|
|
|
1631
1565
|
}
|
|
1632
1566
|
```
|
|
1633
1567
|
|
|
1634
|
-
|
|
1568
|
+
Use the typestate pattern for compile-time state enforcement.
|
|
1635
1569
|
```rust
|
|
1636
1570
|
// States as zero-sized types
|
|
1637
1571
|
struct Draft;
|
|
@@ -1670,7 +1604,7 @@ impl Article<Published> {
|
|
|
1670
1604
|
// article_published.publish(); // Error: method not found
|
|
1671
1605
|
```
|
|
1672
1606
|
|
|
1673
|
-
|
|
1607
|
+
Parse into validated types at system boundaries.
|
|
1674
1608
|
```rust
|
|
1675
1609
|
// Raw input from external source
|
|
1676
1610
|
struct RawUserInput {
|
|
@@ -1705,7 +1639,7 @@ impl Email {
|
|
|
1705
1639
|
}
|
|
1706
1640
|
```
|
|
1707
1641
|
|
|
1708
|
-
|
|
1642
|
+
Make the validated state obvious in function signatures.
|
|
1709
1643
|
```rust
|
|
1710
1644
|
// Functions that require validation communicate it via types
|
|
1711
1645
|
fn send_email(to: &Email, subject: &str, body: &str) -> Result<(), SendError> {
|
|
@@ -1719,7 +1653,7 @@ fn create_account(user: ValidatedUser) -> Result<Account, AccountError> {
|
|
|
1719
1653
|
|
|
1720
1654
|
### Don't
|
|
1721
1655
|
|
|
1722
|
-
|
|
1656
|
+
Scatter validation logic throughout the codebase.
|
|
1723
1657
|
```rust
|
|
1724
1658
|
// Bad: Validation repeated everywhere
|
|
1725
1659
|
fn send_email(to: &str, subject: &str, body: &str) -> Result<(), Error> {
|
|
@@ -1737,7 +1671,7 @@ fn save_user(email: &str) -> Result<(), Error> {
|
|
|
1737
1671
|
}
|
|
1738
1672
|
```
|
|
1739
1673
|
|
|
1740
|
-
|
|
1674
|
+
Allow construction of invalid objects.
|
|
1741
1675
|
```rust
|
|
1742
1676
|
// Bad: Public fields allow invalid state
|
|
1743
1677
|
pub struct Email {
|
|
@@ -1752,7 +1686,7 @@ impl User {
|
|
|
1752
1686
|
}
|
|
1753
1687
|
```
|
|
1754
1688
|
|
|
1755
|
-
|
|
1689
|
+
Use validation functions that return bool.
|
|
1756
1690
|
```rust
|
|
1757
1691
|
// Bad: Caller can ignore the result
|
|
1758
1692
|
fn is_valid_email(s: &str) -> bool {
|
|
@@ -1769,7 +1703,7 @@ fn parse_email(s: &str) -> Result<Email, ValidationError>
|
|
|
1769
1703
|
// Caller must handle the Result
|
|
1770
1704
|
```
|
|
1771
1705
|
|
|
1772
|
-
|
|
1706
|
+
Re-validate already-validated data.
|
|
1773
1707
|
```rust
|
|
1774
1708
|
// Bad: Redundant validation
|
|
1775
1709
|
fn process(user: ValidatedUser) -> Result<(), Error> {
|