Skip to main content

max / synckit

3.6 KB · 119 lines History Blame Raw
1 //! Strongly-typed identifier newtypes.
2 //!
3 //! Every identifier the SDK exchanges with the server is a [`Uuid`], but the
4 //! three that name distinct entities, a device, a user, and an app, are easy
5 //! to transpose at a call site because they share a type. `restore_session`
6 //! takes a user and an app; `store_key` takes an app and a user; both compiled
7 //! happily with the arguments reversed.
8 //!
9 //! These newtypes make that transposition a compile error. They are
10 //! `#[serde(transparent)]`, so the wire format is unchanged, a `DeviceId`
11 //! serializes exactly as the bare `Uuid` it wraps. Conversion in either
12 //! direction is `From`, and [`as_uuid`](DeviceId::as_uuid) hands back the raw
13 //! value when a consumer needs to persist it.
14
15 use serde::{Deserialize, Serialize};
16 use std::fmt;
17 use uuid::Uuid;
18
19 macro_rules! id_newtype {
20 ($(#[$doc:meta])* $name:ident) => {
21 $(#[$doc])*
22 #[derive(
23 Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize,
24 )]
25 #[serde(transparent)]
26 pub struct $name(pub Uuid);
27
28 impl $name {
29 /// Wrap a raw [`Uuid`].
30 pub const fn new(id: Uuid) -> Self {
31 Self(id)
32 }
33
34 /// The underlying [`Uuid`], for persistence or interop with code
35 /// that has not adopted the newtypes.
36 pub const fn as_uuid(self) -> Uuid {
37 self.0
38 }
39
40 /// The nil identifier (all-zero UUID). For a device this is the
41 /// legacy-floor node, an HLC on the nil device always loses, so it
42 /// only ever clocks a pre-HLC entry, never a live device.
43 pub const fn nil() -> Self {
44 Self(Uuid::nil())
45 }
46 }
47
48 impl From<Uuid> for $name {
49 fn from(id: Uuid) -> Self {
50 Self(id)
51 }
52 }
53
54 impl From<$name> for Uuid {
55 fn from(value: $name) -> Self {
56 value.0
57 }
58 }
59
60 impl fmt::Display for $name {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 fmt::Display::fmt(&self.0, f)
63 }
64 }
65 };
66 }
67
68 id_newtype!(
69 /// A SyncKit device, server-assigned at registration.
70 DeviceId
71 );
72 id_newtype!(
73 /// An authenticated MNW user.
74 UserId
75 );
76 id_newtype!(
77 /// A SyncKit application (one per product integrating the SDK).
78 AppId
79 );
80 id_newtype!(
81 /// A SyncKit group: a shared, end-to-end-encrypted changelog several users'
82 /// keys can read and write.
83 GroupId
84 );
85
86 #[cfg(test)]
87 mod tests {
88 use super::*;
89
90 #[test]
91 fn serde_is_transparent_to_uuid() {
92 let raw = Uuid::from_u128(0x1234);
93 let id = DeviceId::new(raw);
94 // A newtype serializes identically to the bare Uuid it wraps.
95 assert_eq!(
96 serde_json::to_string(&id).unwrap(),
97 serde_json::to_string(&raw).unwrap()
98 );
99 // And round-trips from the same wire bytes.
100 let back: DeviceId = serde_json::from_str(&serde_json::to_string(&raw).unwrap()).unwrap();
101 assert_eq!(back, id);
102 }
103
104 #[test]
105 fn conversions_round_trip() {
106 let raw = Uuid::from_u128(7);
107 assert_eq!(Uuid::from(AppId::from(raw)), raw);
108 assert_eq!(UserId::new(raw).as_uuid(), raw);
109 }
110
111 #[test]
112 fn distinct_types_do_not_unify() {
113 // This is a compile-time property; the assertion here is a smoke check
114 // that Display matches the inner Uuid for log/UI parity.
115 let raw = Uuid::from_u128(0xabcd);
116 assert_eq!(DeviceId::new(raw).to_string(), raw.to_string());
117 }
118 }
119