use std::{error::Error, fmt, str::FromStr};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(try_from = "String", into = "String")]
pub struct MachineId(String);
impl MachineId {
pub const HEX_LEN: usize = 32;
pub fn parse(value: &str) -> Result<Self, MachineIdError> {
if value.len() != Self::HEX_LEN {
return Err(MachineIdError::WrongLength { len: value.len() });
}
if let Some(position) = value
.bytes()
.position(|byte| !matches!(byte, b'0'..=b'9' | b'a'..=b'f'))
{
return Err(MachineIdError::NotLowercaseHex { position });
}
Ok(Self(value.to_owned()))
}
pub fn from_bytes(bytes: [u8; 16]) -> Self {
Self(format!("{:032x}", u128::from_be_bytes(bytes)))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for MachineId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for MachineId {
type Err = MachineIdError;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Self::parse(value)
}
}
impl TryFrom<String> for MachineId {
type Error = MachineIdError;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::parse(&value)
}
}
impl From<MachineId> for String {
fn from(value: MachineId) -> Self {
value.0
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MachineIdError {
WrongLength { len: usize },
NotLowercaseHex { position: usize },
}
impl fmt::Display for MachineIdError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::WrongLength { len } => write!(
f,
"a machine id is exactly {} lowercase hex characters, got {len} bytes",
MachineId::HEX_LEN
),
Self::NotLowercaseHex { position } => write!(
f,
"a machine id is exactly {} lowercase hex characters; byte {position} is not 0-9 or a-f",
MachineId::HEX_LEN
),
}
}
}
impl Error for MachineIdError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_exactly_32_lowercase_hex() {
let id = MachineId::parse("0123456789abcdef0123456789abcdef").expect("valid");
assert_eq!(id.as_str(), "0123456789abcdef0123456789abcdef");
}
#[test]
fn refuses_wrong_length_uppercase_and_non_hex() {
assert_eq!(
MachineId::parse("abc"),
Err(MachineIdError::WrongLength { len: 3 })
);
assert_eq!(
MachineId::parse("0123456789abcdef0123456789abcdef\n"),
Err(MachineIdError::WrongLength { len: 33 })
);
assert_eq!(
MachineId::parse("0123456789ABCDEF0123456789abcdef"),
Err(MachineIdError::NotLowercaseHex { position: 10 })
);
assert_eq!(
MachineId::parse("0123456789abcdeg0123456789abcdef"),
Err(MachineIdError::NotLowercaseHex { position: 15 })
);
}
#[test]
fn from_bytes_renders_32_lowercase_hex_with_leading_zeros() {
let mut bytes = [0u8; 16];
bytes[15] = 0xab;
let id = MachineId::from_bytes(bytes);
assert_eq!(id.as_str(), "000000000000000000000000000000ab");
assert_eq!(MachineId::parse(id.as_str()), Ok(id));
}
#[test]
fn serde_refuses_a_malformed_value() {
let ok: MachineId =
serde_json::from_str("\"0123456789abcdef0123456789abcdef\"").expect("valid");
assert_eq!(
serde_json::to_string(&ok).unwrap(),
"\"0123456789abcdef0123456789abcdef\""
);
assert!(serde_json::from_str::<MachineId>("\"not-an-id\"").is_err());
}
}