Type-driven design
Goal
Invalid data cannot be constructed past the crate boundary.
Inputs / outputs
- In: raw input (CLI, config, JSON,
u16,&str) - Out: a type whose constructor is the validation
Reads / writes
- Read: call sites of the untyped value
- Write: none unless a playbook opened the task
Approval
Public API type changes (semver).
RULES — no exceptions
- Parse into a validated type at the edge; inner code takes that type
- Newtype IDs (
UserId(u64)), not raw integers mixed with other IDs - Enums for mutually exclusive states; never
is_ready: bool+result: Option<T>for the same machine Option= absence;Result= failure — do not mixTryFrom/FromStrfor fallible parse;Fromfor infallibleNonZero*when zero is illegal- Typestate only when the compiler must block a call; otherwise an enum is enough
- No stringly APIs (
"admin"vsRole::Admin)
Example
use std::num::NonZeroU16;
struct ServicePort(NonZeroU16);
enum PortError { Zero }
// ❌
fn listen(_port: u16) {}
// ✅
fn listen(_port: ServicePort) {}
impl TryFrom<u16> for ServicePort {
type Error = PortError;
fn try_from(n: u16) -> Result<Self, Self::Error> {
NonZeroU16::new(n).map(Self).ok_or(PortError::Zero)
}
}
Steps
- Name the invariant (non-zero, one of N roles, not both loading and loaded)
- Put it in a type
- Change the function signature; delete scattered
if invalid
Validation
Callers no longer compile if they pass raw unparsed input. Run cargo test covering TryFrom/FromStr error cases (Ok for valid, Err for zero/empty/invalid).
Pitfalls
| ❌ | ✅ |
|---|---|
Port(0) via Option later |
NonZeroU16 |
| Validate in every fn | parse once |
| Typestate for two states | enum |
Integration
Predecessor: rust-essentials. Successor: error-handling.