Files
leonardomso__rust-skills/rules/api-parse-dont-validate.md
2026-06-14 19:38:45 -03:00

187 lines
4.3 KiB
Markdown

# api-parse-dont-validate
> Parse into validated types at boundaries
## Why It Matters
Instead of validating data and hoping you remember to check everywhere, parse it into a type that can only be constructed from valid data. The type system then guarantees validity - you can't forget to validate because invalid states are unrepresentable.
## Bad
```rust
// Validation scattered throughout codebase
fn send_email(email: &str) -> Result<(), Error> {
// Did someone validate this already? Who knows!
if !is_valid_email(email) {
return Err(Error::InvalidEmail);
}
// Send email...
}
fn add_to_mailing_list(email: &str) -> Result<(), Error> {
// Duplicate validation, or did we forget?
if !is_valid_email(email) {
return Err(Error::InvalidEmail);
}
// Add to list...
}
// Easy to forget validation
fn process_user_email(email: &str) {
// Oops, no validation!
database.store_email(email);
}
```
## Good
```rust
/// A validated email address.
/// Can only be constructed via `Email::parse()`.
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Email(String);
impl Email {
/// Parses and validates an email address.
pub fn parse(s: impl Into<String>) -> Result<Self, EmailError> {
let s = s.into();
if Self::is_valid(&s) {
Ok(Email(s))
} else {
Err(EmailError::Invalid)
}
}
fn is_valid(s: &str) -> bool {
s.contains('@') && s.len() > 3 // Simplified
}
pub fn as_str(&self) -> &str {
&self.0
}
}
// Now functions can accept Email - guaranteed valid!
fn send_email(email: &Email) -> Result<(), Error> {
// No validation needed - Email is always valid
smtp_send(email.as_str())
}
fn add_to_mailing_list(email: Email) {
// No validation needed
list.push(email);
}
```
## More Examples
```rust
// Port number (1-65535)
pub struct Port(u16);
impl Port {
pub fn new(n: u16) -> Option<Self> {
if n > 0 { Some(Port(n)) } else { None }
}
pub fn get(&self) -> u16 {
self.0
}
}
// Non-empty string
pub struct NonEmptyString(String);
impl NonEmptyString {
pub fn new(s: impl Into<String>) -> Option<Self> {
let s = s.into();
if s.is_empty() { None } else { Some(Self(s)) }
}
}
// Positive integer
pub struct PositiveI32(i32);
impl PositiveI32 {
pub fn new(n: i32) -> Option<Self> {
if n > 0 { Some(Self(n)) } else { None }
}
}
// Bounded value
pub struct Percentage(u8);
impl Percentage {
pub fn new(n: u8) -> Option<Self> {
if n <= 100 { Some(Self(n)) } else { None }
}
}
```
## Parsing at Boundaries
```rust
// Parse at the system boundary (API, CLI, config file)
fn handle_request(raw: RawRequest) -> Result<Response, Error> {
// Parse ALL inputs upfront
let email = Email::parse(&raw.email)?;
let age = Age::parse(raw.age)?;
let username = Username::parse(&raw.username)?;
// Now work with validated types
process_user(email, age, username)
}
fn process_user(email: Email, age: Age, username: Username) {
// All inputs guaranteed valid - no checks needed
}
```
## Evidence from sqlx
```rust
// sqlx parses SQL at compile time, ensuring query validity
// https://github.com/launchbadge/sqlx/blob/master/src/macros/mod.rs
// The query! macro parses and validates SQL
let user = sqlx::query!("SELECT * FROM users WHERE id = ?", id)
.fetch_one(&pool)
.await?;
// If SQL is invalid, compilation fails - invalid state unrepresentable
```
## Combining with Display
```rust
use std::fmt;
pub struct Email(String);
impl Email {
pub fn parse(s: &str) -> Result<Self, EmailError> { ... }
}
// Implement Display for easy printing
impl fmt::Display for Email {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
// Implement AsRef for easy borrowing
impl AsRef<str> for Email {
fn as_ref(&self) -> &str {
&self.0
}
}
```
## See Also
- [api-newtype-safety](api-newtype-safety.md) - Use newtypes for type safety
- [type-newtype-validated](type-newtype-validated.md) - Newtypes for validated data
- [api-typestate](api-typestate.md) - Compile-time state machines
- [conv-tryfrom-fallible](conv-tryfrom-fallible.md) - Parse via TryFrom
- [serde-try-from-validate](serde-try-from-validate.md) - Validate at the serde boundary