use std::{error::Error, fmt};
use serde::{Deserialize, Serialize};
const MAX_STABLE_ID_LEN: usize = 128;
macro_rules! stable_id {
($name:ident, $description:literal) => {
#[doc = $description]
#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
#[serde(transparent)]
pub struct $name(Box<str>);
impl $name {
pub fn parse(value: impl Into<Box<str>>) -> Result<Self, StableIdError> {
let value = value.into();
validate_stable_id(&value)?;
Ok(Self(value))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::parse(value.into_boxed_str()).map_err(serde::de::Error::custom)
}
}
};
}
stable_id!(
ObservationId,
"Stable identifier for one immutable observation."
);
stable_id!(
SourceId,
"Stable identifier for an evidence-producing source."
);
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum StableIdError {
Empty,
TooLong,
InvalidCharacter,
}
impl fmt::Display for StableIdError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
let message = match self {
Self::Empty => "stable identifier is empty",
Self::TooLong => "stable identifier is too long",
Self::InvalidCharacter => "stable identifier contains an invalid character",
};
formatter.write_str(message)
}
}
impl Error for StableIdError {}
fn validate_stable_id(value: &str) -> Result<(), StableIdError> {
if value.is_empty() {
return Err(StableIdError::Empty);
}
if value.len() > MAX_STABLE_ID_LEN {
return Err(StableIdError::TooLong);
}
if !value.bytes().all(|byte| {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'_' | b'.' | b':' | b'/')
}) {
return Err(StableIdError::InvalidCharacter);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_ids_reject_log_and_protocol_delimiters() {
for invalid in ["", "provider id", "provider\nforged", "provider\"id"] {
assert!(SourceId::parse(invalid).is_err(), "accepted {invalid:?}");
}
}
}