//! Strongly-typed identifier newtypes. //! //! Every identifier the SDK exchanges with the server is a [`Uuid`], but the //! three that name distinct entities, a device, a user, and an app, are easy //! to transpose at a call site because they share a type. `restore_session` //! takes a user and an app; `store_key` takes an app and a user; both compiled //! happily with the arguments reversed. //! //! These newtypes make that transposition a compile error. They are //! `#[serde(transparent)]`, so the wire format is unchanged, a `DeviceId` //! serializes exactly as the bare `Uuid` it wraps. Conversion in either //! direction is `From`, and [`as_uuid`](DeviceId::as_uuid) hands back the raw //! value when a consumer needs to persist it. use serde::{Deserialize, Serialize}; use std::fmt; use uuid::Uuid; macro_rules! id_newtype { ($(#[$doc:meta])* $name:ident) => { $(#[$doc])* #[derive( Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, )] #[serde(transparent)] pub struct $name(pub Uuid); impl $name { /// Wrap a raw [`Uuid`]. pub const fn new(id: Uuid) -> Self { Self(id) } /// The underlying [`Uuid`], for persistence or interop with code /// that has not adopted the newtypes. pub const fn as_uuid(self) -> Uuid { self.0 } /// The nil identifier (all-zero UUID). For a device this is the /// legacy-floor node, an HLC on the nil device always loses, so it /// only ever clocks a pre-HLC entry, never a live device. pub const fn nil() -> Self { Self(Uuid::nil()) } } impl From for $name { fn from(id: Uuid) -> Self { Self(id) } } impl From<$name> for Uuid { fn from(value: $name) -> Self { value.0 } } impl fmt::Display for $name { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Display::fmt(&self.0, f) } } }; } id_newtype!( /// A SyncKit device, server-assigned at registration. DeviceId ); id_newtype!( /// An authenticated MNW user. UserId ); id_newtype!( /// A SyncKit application (one per product integrating the SDK). AppId ); id_newtype!( /// A SyncKit group: a shared, end-to-end-encrypted changelog several users' /// keys can read and write. GroupId ); #[cfg(test)] mod tests { use super::*; #[test] fn serde_is_transparent_to_uuid() { let raw = Uuid::from_u128(0x1234); let id = DeviceId::new(raw); // A newtype serializes identically to the bare Uuid it wraps. assert_eq!( serde_json::to_string(&id).unwrap(), serde_json::to_string(&raw).unwrap() ); // And round-trips from the same wire bytes. let back: DeviceId = serde_json::from_str(&serde_json::to_string(&raw).unwrap()).unwrap(); assert_eq!(back, id); } #[test] fn conversions_round_trip() { let raw = Uuid::from_u128(7); assert_eq!(Uuid::from(AppId::from(raw)), raw); assert_eq!(UserId::new(raw).as_uuid(), raw); } #[test] fn distinct_types_do_not_unify() { // This is a compile-time property; the assertion here is a smoke check // that Display matches the inner Uuid for log/UI parity. let raw = Uuid::from_u128(0xabcd); assert_eq!(DeviceId::new(raw).to_string(), raw.to_string()); } }