Skip to main content

rvoip_core_traits/
ids.rs

1//! Strongly-typed identifiers — the canonical home post-V2.A.
2//!
3//! Each ID is a newtype around a UUID-shaped string so cross-crate
4//! consumers can pattern-match on the kind without confusing a
5//! `SessionId` with a `ConnectionId`. `rvoip-core` re-exports this
6//! whole module so `use rvoip_core::ids::ConnectionId` keeps working.
7
8use serde::{Deserialize, Serialize};
9use std::fmt;
10use uuid::Uuid;
11
12macro_rules! id_type {
13    ($name:ident, $prefix:expr) => {
14        #[derive(Clone, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
15        pub struct $name(pub String);
16
17        impl $name {
18            pub fn new() -> Self {
19                Self(format!("{}_{}", $prefix, Uuid::new_v4().simple()))
20            }
21
22            pub fn from_string(s: impl Into<String>) -> Self {
23                Self(s.into())
24            }
25
26            pub fn as_str(&self) -> &str {
27                &self.0
28            }
29        }
30
31        impl fmt::Display for $name {
32            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
33                f.write_str(&self.0)
34            }
35        }
36
37        impl fmt::Debug for $name {
38            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
39                // IDs are correlation material. Keep their functional Display
40                // and wire forms intact, but make accidental structured-log
41                // capture metadata-only.
42                f.write_str(concat!(stringify!($name), "([redacted])"))
43            }
44        }
45
46        impl Default for $name {
47            fn default() -> Self {
48                Self::new()
49            }
50        }
51    };
52}
53
54id_type!(ConversationId, "conv");
55id_type!(SessionId, "sess");
56id_type!(ConnectionId, "conn");
57id_type!(StreamId, "strm");
58id_type!(MessageId, "msg");
59id_type!(ParticipantId, "part");
60id_type!(IdentityId, "id");
61id_type!(DeviceId, "dev");
62id_type!(BridgeId, "brdg");
63id_type!(MediaRouteId, "route");
64id_type!(TenantId, "tnt");
65id_type!(RecordingId, "rec");
66id_type!(ListenerId, "lstn");
67id_type!(AttachmentId, "att");
68id_type!(AiAttachmentId, "ai");
69id_type!(PlaybackId, "play");
70id_type!(TranscriptionId, "trn");
71id_type!(TransferAttemptId, "xfer");
72
73#[cfg(test)]
74mod diagnostic_tests {
75    use super::*;
76
77    #[test]
78    fn identifier_debug_never_discloses_correlation_values() {
79        const CANARY: &str = "id-diagnostic-canary\r\nAuthorization: exposed";
80        let ids = [
81            format!("{:?}", ConversationId::from_string(CANARY)),
82            format!("{:?}", SessionId::from_string(CANARY)),
83            format!("{:?}", ConnectionId::from_string(CANARY)),
84            format!("{:?}", StreamId::from_string(CANARY)),
85            format!("{:?}", MessageId::from_string(CANARY)),
86            format!("{:?}", TenantId::from_string(CANARY)),
87            format!("{:?}", TransferAttemptId::from_string(CANARY)),
88        ];
89        for debug in ids {
90            assert!(!debug.contains(CANARY));
91            assert!(debug.contains("[redacted]"));
92        }
93        assert_eq!(SessionId::from_string(CANARY).to_string(), CANARY);
94    }
95}