use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CheckId(&'static str);
const ID_DIGITS: usize = 4;
const ID_LEN: usize = 3 + ID_DIGITS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CheckIdError {
Malformed,
}
impl fmt::Display for CheckIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Malformed => {
f.write_str("check id must be `ARC` followed by 4 digits (e.g. `ARC0001`)")
}
}
}
}
impl std::error::Error for CheckIdError {}
impl CheckId {
pub fn new(id: &'static str) -> Result<Self, CheckIdError> {
if is_valid(id) {
Ok(Self(id))
} else {
Err(CheckIdError::Malformed)
}
}
#[must_use]
pub const fn new_unchecked(id: &'static str) -> Self {
Self(id)
}
#[must_use]
pub fn as_str(&self) -> &'static str {
self.0
}
}
impl fmt::Display for CheckId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.0)
}
}
fn is_valid(id: &str) -> bool {
id.len() == ID_LEN
&& id.as_bytes().starts_with(b"ARC")
&& id.as_bytes()[3..].iter().all(|b| b.is_ascii_digit())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_well_formed_ids() {
assert!(CheckId::new("ARC0001").is_ok());
assert!(CheckId::new("ARC9999").is_ok());
assert!(CheckId::new("ARC0042").is_ok());
}
#[test]
fn rejects_malformed_ids() {
assert_eq!(CheckId::new("ARC001"), Err(CheckIdError::Malformed));
assert_eq!(CheckId::new("ARC00001"), Err(CheckIdError::Malformed));
assert_eq!(CheckId::new("arc0001"), Err(CheckIdError::Malformed));
assert_eq!(CheckId::new("ARCabcd"), Err(CheckIdError::Malformed));
assert_eq!(CheckId::new("ARC123"), Err(CheckIdError::Malformed));
assert_eq!(CheckId::new(""), Err(CheckIdError::Malformed));
assert_eq!(CheckId::new("ARC0001 "), Err(CheckIdError::Malformed));
}
#[test]
fn unchecked_is_const_and_round_trips() {
const ID: CheckId = CheckId::new_unchecked("ARC0001");
assert_eq!(ID.as_str(), "ARC0001");
assert_eq!(ID.to_string(), "ARC0001");
}
#[test]
fn ids_are_eq_and_hash_stable() {
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let a = CheckId::new("ARC0001").unwrap();
let b = CheckId::new("ARC0001").unwrap();
assert_eq!(a, b);
let hash_of = |id: CheckId| {
let mut h = DefaultHasher::new();
id.hash(&mut h);
h.finish()
};
assert_eq!(hash_of(a), hash_of(b));
}
#[test]
fn error_displays_helpfully() {
assert_eq!(
CheckIdError::Malformed.to_string(),
"check id must be `ARC` followed by 4 digits (e.g. `ARC0001`)"
);
}
}