Validate Typed Identifiers with Newtype Wrappers

Wrap numeric identifiers in a newtype that parses a prefixed string, rejects zero values, and prints a padded canonical form.

RustData Integrity

Code

snippet
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct UserId(u64);

#[derive(Debug, PartialEq, Eq)]
enum IdError {
    Empty,
    Prefix,
    Digits,
    Zero,
}

impl fmt::Display for IdError {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        let message = match self {
            IdError::Empty => "identifier digits are empty",
            IdError::Prefix => "identifier must start with usr_",
            IdError::Digits => "identifier must contain only digits",
            IdError::Zero => "identifier must be greater than zero",
        };
        formatter.write_str(message)
    }
}

impl FromStr for UserId {
    type Err = IdError;

    fn from_str(raw: &str) -> Result<Self, Self::Err> {
        let digits = raw.strip_prefix("usr_").ok_or(IdError::Prefix)?;
        if digits.is_empty() {
            return Err(IdError::Empty);
        }
        let value: u64 = digits.parse().map_err(|_| IdError::Digits)?;
        if value == 0 {
            return Err(IdError::Zero);
        }
        Ok(UserId(value))
    }
}

impl fmt::Display for UserId {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "usr_{:06}", self.0)
    }
}

fn main() {
    let id: UserId = "usr_004210".parse().expect("valid id");
    println!("{id}");
    assert_eq!(id.to_string(), "usr_004210");
    assert_eq!("usr_000000".parse::<UserId>(), Err(IdError::Zero));
    assert_eq!("4210".parse::<UserId>(), Err(IdError::Prefix));
    assert_eq!("usr_12x".parse::<UserId>(), Err(IdError::Digits));
}