Skip to main content

rvcsi_core/
ids.rs

1//! Identifier value objects.
2//!
3//! `FrameId`, `WindowId` and `EventId` are monotonic `u64` newtypes minted by
4//! an [`IdGenerator`]. `SessionId` is also a `u64` (one per capture session).
5//! `SourceId` wraps a human-readable string (`"esp32-com7"`, `"pcap:lab.pcap"`)
6//! so logs and RuVector records stay legible.
7
8use std::sync::atomic::{AtomicU64, Ordering};
9
10use serde::{Deserialize, Serialize};
11
12macro_rules! u64_newtype {
13    ($(#[$m:meta])* $name:ident) => {
14        $(#[$m])*
15        #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
16        pub struct $name(pub u64);
17
18        impl $name {
19            /// The raw integer value.
20            #[inline]
21            pub const fn value(self) -> u64 {
22                self.0
23            }
24        }
25
26        impl From<u64> for $name {
27            #[inline]
28            fn from(v: u64) -> Self {
29                $name(v)
30            }
31        }
32
33        impl core::fmt::Display for $name {
34            fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
35                write!(f, "{}#{}", stringify!($name), self.0)
36            }
37        }
38    };
39}
40
41u64_newtype!(
42    /// Identifies one CSI observation within a capture session.
43    FrameId
44);
45u64_newtype!(
46    /// Identifies a capture session (one source + one runtime config).
47    SessionId
48);
49u64_newtype!(
50    /// Identifies a bounded window of frames.
51    WindowId
52);
53u64_newtype!(
54    /// Identifies a semantic event.
55    EventId
56);
57
58/// Human-readable identifier for a CSI source.
59#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
60pub struct SourceId(pub String);
61
62impl SourceId {
63    /// Construct from anything string-like.
64    pub fn new(s: impl Into<String>) -> Self {
65        SourceId(s.into())
66    }
67
68    /// Borrow the underlying string.
69    pub fn as_str(&self) -> &str {
70        &self.0
71    }
72}
73
74impl From<&str> for SourceId {
75    fn from(s: &str) -> Self {
76        SourceId(s.to_string())
77    }
78}
79
80impl From<String> for SourceId {
81    fn from(s: String) -> Self {
82        SourceId(s)
83    }
84}
85
86impl core::fmt::Display for SourceId {
87    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
88        f.write_str(&self.0)
89    }
90}
91
92/// Monotonic id minter shared by a runtime instance.
93///
94/// Frame, window and event id spaces are independent. The generator is
95/// `Send + Sync` (atomic counters) so it can be shared across the capture,
96/// signal and event tasks.
97#[derive(Debug, Default)]
98pub struct IdGenerator {
99    frame: AtomicU64,
100    window: AtomicU64,
101    event: AtomicU64,
102    session: AtomicU64,
103}
104
105impl IdGenerator {
106    /// A fresh generator with all counters at zero.
107    pub const fn new() -> Self {
108        IdGenerator {
109            frame: AtomicU64::new(0),
110            window: AtomicU64::new(0),
111            event: AtomicU64::new(0),
112            session: AtomicU64::new(0),
113        }
114    }
115
116    /// Next frame id.
117    pub fn next_frame(&self) -> FrameId {
118        FrameId(self.frame.fetch_add(1, Ordering::Relaxed))
119    }
120
121    /// Next window id.
122    pub fn next_window(&self) -> WindowId {
123        WindowId(self.window.fetch_add(1, Ordering::Relaxed))
124    }
125
126    /// Next event id.
127    pub fn next_event(&self) -> EventId {
128        EventId(self.event.fetch_add(1, Ordering::Relaxed))
129    }
130
131    /// Next session id.
132    pub fn next_session(&self) -> SessionId {
133        SessionId(self.session.fetch_add(1, Ordering::Relaxed))
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn id_generator_is_monotonic_and_independent() {
143        let g = IdGenerator::new();
144        assert_eq!(g.next_frame(), FrameId(0));
145        assert_eq!(g.next_frame(), FrameId(1));
146        assert_eq!(g.next_window(), WindowId(0));
147        assert_eq!(g.next_event(), EventId(0));
148        assert_eq!(g.next_frame(), FrameId(2));
149        assert_eq!(g.next_session(), SessionId(0));
150    }
151
152    #[test]
153    fn source_id_roundtrips_and_displays() {
154        let s = SourceId::from("esp32-com7");
155        assert_eq!(s.as_str(), "esp32-com7");
156        assert_eq!(s.to_string(), "esp32-com7");
157        let json = serde_json::to_string(&s).unwrap();
158        assert_eq!(serde_json::from_str::<SourceId>(&json).unwrap(), s);
159    }
160
161    #[test]
162    fn u64_newtype_display_and_serde() {
163        let f = FrameId(42);
164        assert_eq!(f.value(), 42);
165        assert_eq!(f.to_string(), "FrameId#42");
166        let json = serde_json::to_string(&f).unwrap();
167        assert_eq!(json, "42");
168        assert_eq!(serde_json::from_str::<FrameId>(&json).unwrap(), f);
169    }
170}