installation.rs (1863B)
1 use harvestcircle_domain::{SafeError, SafeErrorCode, SafeMessage}; 2 3 #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] 4 pub struct InstallationIdentity(String); 5 6 impl InstallationIdentity { 7 pub fn parse(value: impl Into<String>) -> Result<Self, SafeError> { 8 let value = value.into(); 9 if value.len() != 32 10 || !value 11 .bytes() 12 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) 13 { 14 return Err(invalid_installation_identity()); 15 } 16 Ok(Self(value)) 17 } 18 19 #[must_use] 20 pub fn as_str(&self) -> &str { 21 self.0.as_str() 22 } 23 } 24 25 pub trait InstallationIdentitySource: Send + Sync { 26 fn generate(&self) -> Result<InstallationIdentity, SafeError>; 27 } 28 29 #[derive(Clone, Copy, Debug, Default)] 30 pub struct UuidInstallationIdentitySource; 31 32 impl InstallationIdentitySource for UuidInstallationIdentitySource { 33 fn generate(&self) -> Result<InstallationIdentity, SafeError> { 34 InstallationIdentity::parse(uuid::Uuid::new_v4().simple().to_string()) 35 } 36 } 37 38 const fn invalid_installation_identity() -> SafeError { 39 SafeError::new( 40 SafeErrorCode::InvalidApplicationState, 41 SafeMessage::new("The installation identity is invalid."), 42 ) 43 } 44 45 #[cfg(test)] 46 mod tests { 47 use super::{InstallationIdentity, InstallationIdentitySource, UuidInstallationIdentitySource}; 48 49 #[test] 50 fn installation_identity_is_fixed_width_lowercase_hex() { 51 let identity = UuidInstallationIdentitySource.generate().expect("identity"); 52 assert_eq!(identity.as_str().len(), 32); 53 assert!(InstallationIdentity::parse(identity.as_str()).is_ok()); 54 for denied in ["", "AAaabbccddeeff001122334455667788", "not-an-identity"] { 55 assert!(InstallationIdentity::parse(denied).is_err()); 56 } 57 } 58 }