Skip to main content

vr_core/
ids.rs

1//! Type-safe ID newtypes for the PRPaaS domain.
2//!
3//! Every entity gets its own NexId newtype to prevent accidental mixing
4//! (e.g., passing a TenantId where a UserId is expected is a compile error).
5
6use nexcore_id::NexId;
7use serde::{Deserialize, Serialize};
8use std::fmt;
9
10macro_rules! define_id {
11    ($name:ident, $prefix:expr) => {
12        #[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13        #[serde(transparent)]
14        pub struct $name(NexId);
15
16        impl $name {
17            #[must_use]
18            pub fn new() -> Self {
19                Self(NexId::v4())
20            }
21
22            #[must_use]
23            pub fn from_nexid(id: NexId) -> Self {
24                Self(id)
25            }
26
27            #[must_use]
28            pub fn as_nexid(&self) -> &NexId {
29                &self.0
30            }
31
32            #[must_use]
33            pub fn into_nexid(self) -> NexId {
34                self.0
35            }
36
37            /// Parse from string, returning None on invalid UUID.
38            #[must_use]
39            pub fn parse(s: &str) -> Option<Self> {
40                s.parse::<NexId>().ok().map(Self)
41            }
42        }
43
44        impl Default for $name {
45            fn default() -> Self {
46                Self::new()
47            }
48        }
49
50        impl fmt::Display for $name {
51            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52                write!(f, "{}_{}", $prefix, self.0)
53            }
54        }
55
56        impl fmt::Debug for $name {
57            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58                write!(f, "{}({})", stringify!($name), self.0)
59            }
60        }
61
62        impl From<NexId> for $name {
63            fn from(id: NexId) -> Self {
64                Self(id)
65            }
66        }
67
68        impl From<$name> for NexId {
69            fn from(id: $name) -> Self {
70                id.0
71            }
72        }
73    };
74}
75
76define_id!(TenantId, "ten");
77define_id!(UserId, "usr");
78define_id!(ProgramId, "prg");
79define_id!(CompoundId, "cpd");
80define_id!(AssayId, "asy");
81define_id!(OrderId, "ord");
82define_id!(ProviderId, "prv");
83define_id!(ModelId, "mdl");
84define_id!(InvoiceId, "inv");
85define_id!(SignalId, "sig");
86define_id!(DealId, "deal");
87define_id!(AssetId, "ast");
88define_id!(TerminalSessionId, "tsn");
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn ids_are_not_interchangeable() {
97        let tenant = TenantId::new();
98        let user = UserId::new();
99        assert_ne!(tenant.as_nexid(), user.as_nexid());
100    }
101
102    #[test]
103    fn display_includes_prefix() {
104        let id = TenantId::from_nexid(NexId::NIL);
105        assert!(id.to_string().starts_with("ten_"));
106    }
107
108    #[test]
109    fn roundtrip_serde() {
110        let id = TenantId::new();
111        let json = serde_json::to_string(&id).unwrap();
112        let back: TenantId = serde_json::from_str(&json).unwrap();
113        assert_eq!(id, back);
114    }
115
116    #[test]
117    fn parse_valid_uuid() {
118        let id = TenantId::new();
119        let uuid_str = id.as_nexid().to_string();
120        let parsed = TenantId::parse(&uuid_str);
121        assert_eq!(parsed, Some(id));
122    }
123
124    #[test]
125    fn parse_invalid_returns_none() {
126        assert_eq!(TenantId::parse("not-a-uuid"), None);
127    }
128}