use core::fmt;
use core::marker::PhantomData;
use serde::de::{self, Visitor};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use uuid::Uuid;
use crate::error::IdError;
pub trait StableIdKind: sealed::Sealed + 'static {
const TAG: &'static str;
}
mod sealed {
pub trait Sealed {}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NodeTag;
impl sealed::Sealed for NodeTag {}
impl StableIdKind for NodeTag {
const TAG: &'static str = "node";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct EdgeTag;
impl sealed::Sealed for EdgeTag {}
impl StableIdKind for EdgeTag {
const TAG: &'static str = "edge";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ChangeTag;
impl sealed::Sealed for ChangeTag {}
impl StableIdKind for ChangeTag {
const TAG: &'static str = "change";
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct OperationTag;
impl sealed::Sealed for OperationTag {}
impl StableIdKind for OperationTag {
const TAG: &'static str = "op";
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct StableId<Kind: StableIdKind> {
bytes: [u8; 16],
_tag: PhantomData<fn() -> Kind>,
}
impl<Kind: StableIdKind> Serialize for StableId<Kind> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.bytes)
}
}
struct StableIdVisitor<Kind>(PhantomData<fn() -> Kind>);
impl<'de, Kind: StableIdKind> Visitor<'de> for StableIdVisitor<Kind> {
type Value = StableId<Kind>;
fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("a 16-byte stable-id byte string")
}
fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
if v.len() != 16 {
return Err(E::invalid_length(v.len(), &"16"));
}
let mut arr = [0u8; 16];
arr.copy_from_slice(v);
Ok(StableId::from_bytes_raw(arr))
}
fn visit_borrowed_bytes<E: de::Error>(self, v: &'de [u8]) -> Result<Self::Value, E> {
self.visit_bytes(v)
}
fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
self.visit_bytes(&v)
}
}
impl<'de, Kind: StableIdKind> Deserialize<'de> for StableId<Kind> {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_bytes(StableIdVisitor::<Kind>(PhantomData))
}
}
impl<Kind: StableIdKind> StableId<Kind> {
#[must_use]
pub fn new_v7() -> Self {
Self::from_bytes_raw(*Uuid::now_v7().as_bytes())
}
#[must_use]
pub const fn from_bytes_raw(bytes: [u8; 16]) -> Self {
Self {
bytes,
_tag: PhantomData,
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, IdError> {
let arr: [u8; 16] = bytes
.try_into()
.map_err(|_| IdError::StableIdLength { got: bytes.len() })?;
Ok(Self::from_bytes_raw(arr))
}
pub fn parse_uuid(s: &str) -> Result<Self, IdError> {
Uuid::parse_str(s)
.map(|u| Self::from_bytes_raw(*u.as_bytes()))
.map_err(|source| IdError::StableIdParse { source })
}
#[must_use]
pub const fn from_random_bytes(bytes: [u8; 16]) -> Self {
Self::from_bytes_raw(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 16] {
&self.bytes
}
#[must_use]
pub const fn into_bytes(self) -> [u8; 16] {
self.bytes
}
#[must_use]
pub fn to_uuid_string(&self) -> String {
Uuid::from_bytes(self.bytes).hyphenated().to_string()
}
}
impl<Kind: StableIdKind> fmt::Debug for StableId<Kind> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}({})", Kind::TAG, self.to_uuid_string())
}
}
impl<Kind: StableIdKind> fmt::Display for StableId<Kind> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_uuid_string())
}
}
pub type NodeId = StableId<NodeTag>;
pub type EdgeId = StableId<EdgeTag>;
pub type ChangeId = StableId<ChangeTag>;
pub type OperationId = StableId<OperationTag>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn stable_ids_have_distinct_types() {
let n = NodeId::new_v7();
let bytes = *n.as_bytes();
let n2 = NodeId::from_bytes(&bytes).expect("16 bytes");
assert_eq!(n, n2);
}
#[test]
fn stable_id_debug_shows_kind_tag() {
let n = NodeId::new_v7();
let s = format!("{n:?}");
assert!(
s.starts_with("node("),
"debug repr begins with kind tag: {s}"
);
}
#[test]
fn wrong_length_rejected() {
let err = NodeId::from_bytes(&[0u8; 8]).unwrap_err();
match err {
IdError::StableIdLength { got } => assert_eq!(got, 8),
e => panic!("wrong variant: {e:?}"),
}
}
#[test]
fn uuid_string_roundtrip() {
let n = NodeId::new_v7();
let s = n.to_uuid_string();
let parsed = NodeId::parse_uuid(&s).expect("valid uuid");
assert_eq!(n, parsed);
}
#[test]
fn uuidv7_is_time_ordered_within_a_ms() {
let mut ids: Vec<NodeId> = (0..32).map(|_| NodeId::new_v7()).collect();
let sorted = {
let mut c = ids.clone();
c.sort();
c
};
for window in sorted.windows(2) {
let a = &window[0].as_bytes()[0..6];
let b = &window[1].as_bytes()[0..6];
assert!(
a <= b,
"UUIDv7 timestamp prefix non-monotonic: {a:?} > {b:?}"
);
}
ids.clear();
}
}