use core::fmt;
pub use uuid::Uuid;
const HYPHENATED_LEN: usize = 36;
const REQUIRED_VERSION: usize = 7;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IdentifierError {
type_name: &'static str,
detail: String,
}
impl IdentifierError {
#[must_use]
pub const fn type_name(&self) -> &'static str {
self.type_name
}
#[must_use]
pub fn detail(&self) -> &str {
&self.detail
}
}
impl fmt::Display for IdentifierError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
formatter,
"{} is not valid: {}",
self.type_name, self.detail
)
}
}
impl core::error::Error for IdentifierError {}
pub fn parse_identifier(type_name: &'static str, text: &str) -> Result<Uuid, IdentifierError> {
let refuse = |detail: String| IdentifierError { type_name, detail };
if text.len() != HYPHENATED_LEN {
return Err(refuse(format!(
"expected the lowercase hyphenated spelling of {HYPHENATED_LEN} characters, \
found {} characters in {text:?}",
text.len()
)));
}
if text.chars().any(|character| character.is_ascii_uppercase()) {
return Err(refuse(format!(
"expected the lowercase spelling, found an uppercase character in {text:?}"
)));
}
let uuid = Uuid::parse_str(text)
.map_err(|error| refuse(format!("{text:?} is not a UUID: {error}")))?;
if uuid.get_version_num() != REQUIRED_VERSION {
return Err(refuse(format!(
"expected a version {REQUIRED_VERSION} UUID, found version {} in {text:?}",
uuid.get_version_num()
)));
}
Ok(uuid)
}
#[macro_export]
macro_rules! identifier {
($name:ident, $what:literal) => {
#[doc = concat!("PrintObserver's own identifier for ", $what, ".")]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name($crate::ids::Uuid);
impl $name {
#[must_use]
pub fn new() -> Self {
Self($crate::ids::Uuid::now_v7())
}
#[must_use]
pub const fn as_uuid(&self) -> &$crate::ids::Uuid {
&self.0
}
}
impl ::core::default::Default for $name {
fn default() -> Self {
Self::new()
}
}
impl ::core::fmt::Display for $name {
fn fmt(&self, formatter: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
::core::write!(formatter, "{}", self.0.as_hyphenated())
}
}
impl ::core::str::FromStr for $name {
type Err = $crate::IdentifierError;
fn from_str(text: &str) -> ::core::result::Result<Self, Self::Err> {
$crate::ids::parse_identifier(stringify!($name), text).map(Self)
}
}
impl $crate::serde::Serialize for $name {
fn serialize<S: $crate::serde::Serializer>(
&self,
serializer: S,
) -> ::core::result::Result<S::Ok, S::Error> {
serializer.collect_str(&self.0.as_hyphenated())
}
}
impl<'de> $crate::serde::Deserialize<'de> for $name {
fn deserialize<D: $crate::serde::Deserializer<'de>>(
deserializer: D,
) -> ::core::result::Result<Self, D::Error> {
let text = <::std::string::String as $crate::serde::Deserialize>::deserialize(
deserializer,
)?;
text.parse()
.map_err(<D::Error as $crate::serde::de::Error>::custom)
}
}
impl $crate::schemars::JsonSchema for $name {
fn schema_name() -> ::std::borrow::Cow<'static, str> {
::std::borrow::Cow::Borrowed(stringify!($name))
}
fn json_schema(
_generator: &mut $crate::schemars::SchemaGenerator,
) -> $crate::schemars::Schema {
$crate::schemars::json_schema!({
"type": "string",
"format": "uuid",
"title": stringify!($name),
"description": concat!(
"A lowercase hyphenated version 7 UUID identifying ", $what, "."
),
"pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$"
})
}
}
};
}
identifier!(PrintId, "one print");
identifier!(EventId, "one event");
identifier!(ImageId, "one image");