#![forbid(unsafe_code)]
use super::error::DomainError;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use std::fmt;
use uuid::{Uuid, Variant, Version};
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CorrelationId(Uuid);
#[repr(transparent)]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct BatchRunId(Uuid);
fn reject_sentinel(u: Uuid) -> Result<Uuid, DomainError> {
if u.is_nil() {
return Err(DomainError::new("uuid", "nil UUID is a forbidden sentinel"));
}
if u.as_u128() == u128::MAX {
return Err(DomainError::new("uuid", "max UUID is a forbidden sentinel"));
}
match u.get_variant() {
Variant::RFC4122 => {}
_ => {
return Err(DomainError::new(
"uuid",
"UUID variant must be RFC 4122/9562",
));
}
}
Ok(u)
}
impl CorrelationId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::new_v4())
}
pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
let u = Uuid::parse_str(raw.as_ref().trim())
.map_err(|e| DomainError::new("correlation_id", e.to_string()))?;
let u = reject_sentinel(u)?;
if let Some(v) = u.get_version() {
if v != Version::Random {
return Err(DomainError::new(
"correlation_id",
format!("expected UUID v4, got {v:?}"),
));
}
}
Ok(Self(u))
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
#[must_use]
pub fn to_string_canonical(&self) -> String {
self.0.to_string()
}
}
impl Default for CorrelationId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for CorrelationId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Serialize for CorrelationId {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for CorrelationId {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::try_new(s).map_err(serde::de::Error::custom)
}
}
impl BatchRunId {
#[must_use]
pub fn new() -> Self {
Self(Uuid::now_v7())
}
pub fn try_new(raw: impl AsRef<str>) -> Result<Self, DomainError> {
let u = Uuid::parse_str(raw.as_ref().trim())
.map_err(|e| DomainError::new("batch_run_id", e.to_string()))?;
let u = reject_sentinel(u)?;
if let Some(v) = u.get_version() {
if v != Version::SortRand {
return Err(DomainError::new(
"batch_run_id",
format!("expected UUID v7, got {v:?}"),
));
}
}
Ok(Self(u))
}
#[must_use]
pub const fn as_uuid(&self) -> &Uuid {
&self.0
}
#[must_use]
pub fn to_string_canonical(&self) -> String {
self.0.to_string()
}
}
impl Default for BatchRunId {
fn default() -> Self {
Self::new()
}
}
impl fmt::Display for BatchRunId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl Serialize for BatchRunId {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.0.to_string())
}
}
impl<'de> Deserialize<'de> for BatchRunId {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let s = String::deserialize(d)?;
Self::try_new(s).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn correlation_v4_roundtrip() {
let id = CorrelationId::new();
let s = id.to_string_canonical();
let back = CorrelationId::try_new(&s).unwrap();
assert_eq!(id, back);
assert_eq!(id.as_uuid().get_version(), Some(Version::Random));
}
#[test]
fn batch_v7_roundtrip() {
let a = BatchRunId::new();
let b = BatchRunId::new();
let s = a.to_string_canonical();
let back = BatchRunId::try_new(&s).unwrap();
assert_eq!(a, back);
assert_eq!(a.as_uuid().get_version(), Some(Version::SortRand));
let _ = b;
}
#[test]
fn rejects_nil() {
assert!(CorrelationId::try_new("00000000-0000-0000-0000-000000000000").is_err());
assert!(BatchRunId::try_new("00000000-0000-0000-0000-000000000000").is_err());
}
#[test]
fn distinct_types_not_interchangeable() {
let c = CorrelationId::new();
assert!(BatchRunId::try_new(c.to_string_canonical()).is_err());
}
}