use core::fmt;
use core::str::FromStr;
use std::borrow::Cow;
use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
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 {}
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_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(Uuid);
impl $name {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
}
impl Default for $name {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for $name {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(formatter, "{}", self.0.as_hyphenated())
}
}
impl FromStr for $name {
type Err = IdentifierError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
parse_identifier(stringify!($name), text).map(Self)
}
}
impl Serialize for $name {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.collect_str(&self.0.as_hyphenated())
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let text = String::deserialize(deserializer)?;
text.parse().map_err(D::Error::custom)
}
}
impl JsonSchema for $name {
fn schema_name() -> Cow<'static, str> {
Cow::Borrowed(stringify!($name))
}
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
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");
identifier!(ActionId, "one requested action");
identifier!(InterventionId, "one bounded intervention");