Skip to main content

synapse_primitives/
id.rs

1//! Strongly-typed identifiers with SipHash integration
2//!
3//! This module provides type-safe wrappers around numeric IDs that are derived
4//! from string names via SipHash. This provides both efficiency (4-byte IDs on wire)
5//! and type safety (can't mix up different ID types).
6//!
7//! # Examples
8//!
9//! ```
10//! use synapse_primitives::id::{InterfaceId, MethodId, ServiceId, HeaderKeyId};
11//!
12//! // Create IDs from names
13//! let interface_id = InterfaceId::from_name("mensa.user.v2.UserInterface");
14//! let method_id = MethodId::from_name("GetUser");
15//! let service_id = ServiceId::from_name("user-service");
16//! let header_id = HeaderKeyId::from_name("trace_id");
17//!
18//! // Use in protobuf or network protocols
19//! let wire_format: u32 = interface_id.into();
20//! let restored = InterfaceId::from_raw(wire_format);
21//! assert_eq!(interface_id, restored);
22//! ```
23
24use crate::siphash::hash_name_u32;
25use serde::{Deserialize, Serialize};
26use std::fmt;
27
28/// Service identifier - represents a logical service unit
29///
30/// Services are hashed from their names (e.g., "user-service", "payment-service")
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
32pub struct ServiceId(u32);
33
34impl ServiceId {
35    /// Create a ServiceId from a service name
36    ///
37    /// # Examples
38    ///
39    /// ```
40    /// use synapse_primitives::id::ServiceId;
41    ///
42    /// let id = ServiceId::from_name("user-service");
43    /// ```
44    pub fn from_name(name: &str) -> Self {
45        Self(hash_name_u32(name))
46    }
47
48    /// Create from raw u32 (e.g., from wire protocol)
49    pub const fn from_raw(id: u32) -> Self {
50        Self(id)
51    }
52
53    /// Get the raw u32 value
54    pub const fn as_u32(&self) -> u32 {
55        self.0
56    }
57}
58
59impl From<ServiceId> for u32 {
60    fn from(id: ServiceId) -> u32 {
61        id.0
62    }
63}
64
65impl From<u32> for ServiceId {
66    fn from(id: u32) -> ServiceId {
67        ServiceId(id)
68    }
69}
70
71impl fmt::Display for ServiceId {
72    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73        write!(f, "ServiceId(0x{:08X})", self.0)
74    }
75}
76
77/// Interface identifier - represents a protobuf-defined RPC interface
78///
79/// Interfaces are hashed from their fully-qualified names, including version:
80/// e.g., "mensa.user.v2.UserInterface"
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
82pub struct InterfaceId(u32);
83
84impl InterfaceId {
85    /// Create an InterfaceId from a fully-qualified interface name
86    ///
87    /// # Examples
88    ///
89    /// ```
90    /// use synapse_primitives::id::InterfaceId;
91    ///
92    /// let id = InterfaceId::from_name("mensa.user.v2.UserInterface");
93    /// ```
94    pub fn from_name(name: &str) -> Self {
95        Self(hash_name_u32(name))
96    }
97
98    /// Create from raw u32 (e.g., from wire protocol)
99    pub const fn from_raw(id: u32) -> Self {
100        Self(id)
101    }
102
103    /// Get the raw u32 value
104    pub const fn as_u32(&self) -> u32 {
105        self.0
106    }
107}
108
109impl From<InterfaceId> for u32 {
110    fn from(id: InterfaceId) -> u32 {
111        id.0
112    }
113}
114
115impl From<u32> for InterfaceId {
116    fn from(id: u32) -> InterfaceId {
117        InterfaceId(id)
118    }
119}
120
121impl fmt::Display for InterfaceId {
122    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
123        write!(f, "InterfaceId(0x{:08X})", self.0)
124    }
125}
126
127/// Method identifier - represents a method within an RPC interface
128///
129/// Methods are hashed from their names (e.g., "GetUser", "CreateUser")
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
131pub struct MethodId(u32);
132
133impl MethodId {
134    /// Create a MethodId from a method name
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use synapse_primitives::id::MethodId;
140    ///
141    /// let id = MethodId::from_name("GetUser");
142    /// ```
143    pub fn from_name(name: &str) -> Self {
144        Self(hash_name_u32(name))
145    }
146
147    /// Create from raw u32 (e.g., from wire protocol)
148    pub const fn from_raw(id: u32) -> Self {
149        Self(id)
150    }
151
152    /// Get the raw u32 value
153    pub const fn as_u32(&self) -> u32 {
154        self.0
155    }
156}
157
158impl From<MethodId> for u32 {
159    fn from(id: MethodId) -> u32 {
160        id.0
161    }
162}
163
164impl From<u32> for MethodId {
165    fn from(id: u32) -> MethodId {
166        MethodId(id)
167    }
168}
169
170impl fmt::Display for MethodId {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "MethodId(0x{:08X})", self.0)
173    }
174}
175
176/// Header key identifier - represents a header name in RPC messages
177///
178/// Header keys are hashed from their names (e.g., "trace_id", "request_id")
179#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
180pub struct HeaderKeyId(u32);
181
182impl HeaderKeyId {
183    /// Create a HeaderKeyId from a header name
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use synapse_primitives::id::HeaderKeyId;
189    ///
190    /// let trace_id = HeaderKeyId::from_name("trace_id");
191    /// let request_id = HeaderKeyId::from_name("request_id");
192    /// ```
193    pub fn from_name(name: &str) -> Self {
194        Self(hash_name_u32(name))
195    }
196
197    /// Create from raw u32 (e.g., from wire protocol)
198    pub const fn from_raw(id: u32) -> Self {
199        Self(id)
200    }
201
202    /// Get the raw u32 value
203    pub const fn as_u32(&self) -> u32 {
204        self.0
205    }
206}
207
208impl From<HeaderKeyId> for u32 {
209    fn from(id: HeaderKeyId) -> u32 {
210        id.0
211    }
212}
213
214impl From<u32> for HeaderKeyId {
215    fn from(id: u32) -> HeaderKeyId {
216        HeaderKeyId(id)
217    }
218}
219
220impl fmt::Display for HeaderKeyId {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        write!(f, "HeaderKeyId(0x{:08X})", self.0)
223    }
224}
225
226/// Metric identifier - represents a metric name for monitoring
227///
228/// Metrics are hashed from their names (e.g., "request_count", "request_duration_ms")
229#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
230pub struct MetricId(u32);
231
232impl MetricId {
233    /// Create a MetricId from a metric name
234    ///
235    /// # Examples
236    ///
237    /// ```
238    /// use synapse_primitives::id::MetricId;
239    ///
240    /// let request_count = MetricId::from_name("request_count");
241    /// let error_count = MetricId::from_name("error_count");
242    /// ```
243    pub fn from_name(name: &str) -> Self {
244        Self(hash_name_u32(name))
245    }
246
247    /// Create from raw u32 (e.g., from wire protocol)
248    pub const fn from_raw(id: u32) -> Self {
249        Self(id)
250    }
251
252    /// Get the raw u32 value
253    pub const fn as_u32(&self) -> u32 {
254        self.0
255    }
256}
257
258impl From<MetricId> for u32 {
259    fn from(id: MetricId) -> u32 {
260        id.0
261    }
262}
263
264impl From<u32> for MetricId {
265    fn from(id: u32) -> MetricId {
266        MetricId(id)
267    }
268}
269
270impl fmt::Display for MetricId {
271    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272        write!(f, "MetricId(0x{:08X})", self.0)
273    }
274}
275
276/// Instance identifier - represents a specific running instance of a service
277///
278/// Instances use 128-bit UUIDs for globally unique identification
279#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
280pub struct InstanceId(u128);
281
282impl InstanceId {
283    /// Create a new random instance ID using UUID v4
284    pub fn new_random() -> Self {
285        let uuid = uuid::Uuid::new_v4();
286        Self(u128::from_be_bytes(*uuid.as_bytes()))
287    }
288
289    /// Create from raw u128
290    pub const fn from_raw(id: u128) -> Self {
291        Self(id)
292    }
293
294    /// Create from bytes
295    pub fn from_bytes(bytes: [u8; 16]) -> Self {
296        Self(u128::from_be_bytes(bytes))
297    }
298
299    /// Get the raw u128 value
300    pub const fn as_u128(&self) -> u128 {
301        self.0
302    }
303
304    /// Get as bytes
305    pub fn as_bytes(&self) -> [u8; 16] {
306        self.0.to_be_bytes()
307    }
308}
309
310impl From<InstanceId> for u128 {
311    fn from(id: InstanceId) -> u128 {
312        id.0
313    }
314}
315
316impl From<u128> for InstanceId {
317    fn from(id: u128) -> InstanceId {
318        InstanceId(id)
319    }
320}
321
322impl fmt::Display for InstanceId {
323    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
324        write!(f, "InstanceId(0x{:032X})", self.0)
325    }
326}
327
328/// Common header key IDs for standard headers
329///
330/// Values are precomputed from `HeaderKeyId::from_name()` (SipHash) to ensure
331/// these constants match runtime-computed values exactly.
332pub mod well_known {
333    use super::HeaderKeyId;
334    use once_cell::sync::Lazy;
335
336    /// Standard header: trace_id
337    pub static TRACE_ID: Lazy<HeaderKeyId> = Lazy::new(|| HeaderKeyId::from_name("trace_id"));
338
339    /// Standard header: span_id
340    pub static SPAN_ID: Lazy<HeaderKeyId> = Lazy::new(|| HeaderKeyId::from_name("span_id"));
341
342    /// Standard header: request_id
343    pub static REQUEST_ID: Lazy<HeaderKeyId> = Lazy::new(|| HeaderKeyId::from_name("request_id"));
344
345    /// Standard header: caller_service
346    pub static CALLER_SERVICE: Lazy<HeaderKeyId> =
347        Lazy::new(|| HeaderKeyId::from_name("caller_service"));
348
349    /// Standard header: idempotency_key
350    pub static IDEMPOTENCY_KEY: Lazy<HeaderKeyId> =
351        Lazy::new(|| HeaderKeyId::from_name("idempotency_key"));
352}
353
354#[cfg(test)]
355mod tests {
356    use super::*;
357
358    #[test]
359    fn test_service_id() {
360        let id1 = ServiceId::from_name("user-service");
361        let id2 = ServiceId::from_name("user-service");
362        let id3 = ServiceId::from_name("payment-service");
363
364        assert_eq!(id1, id2, "Same name should produce same ID");
365        assert_ne!(id1, id3, "Different names should produce different IDs");
366
367        // Round-trip through u32
368        let raw: u32 = id1.into();
369        let restored = ServiceId::from_raw(raw);
370        assert_eq!(id1, restored);
371    }
372
373    #[test]
374    fn test_interface_id() {
375        let id = InterfaceId::from_name("mensa.user.v2.UserInterface");
376        let raw = id.as_u32();
377        let restored = InterfaceId::from_raw(raw);
378        assert_eq!(id, restored);
379    }
380
381    #[test]
382    fn test_method_id() {
383        let get_user = MethodId::from_name("GetUser");
384        let create_user = MethodId::from_name("CreateUser");
385        assert_ne!(get_user, create_user);
386    }
387
388    #[test]
389    fn test_header_key_id() {
390        let trace_id = HeaderKeyId::from_name("trace_id");
391        let request_id = HeaderKeyId::from_name("request_id");
392        assert_ne!(trace_id, request_id);
393    }
394
395    #[test]
396    fn test_metric_id() {
397        let request_count = MetricId::from_name("request_count");
398        let error_count = MetricId::from_name("error_count");
399        assert_ne!(request_count, error_count);
400    }
401
402    #[test]
403    fn test_instance_id() {
404        let id1 = InstanceId::new_random();
405
406        // Round-trip through bytes
407        let bytes = id1.as_bytes();
408        let restored = InstanceId::from_bytes(bytes);
409        assert_eq!(id1, restored);
410
411        // Verify size
412        assert_eq!(bytes.len(), 16);
413    }
414
415    #[test]
416    fn test_display() {
417        let service_id = ServiceId::from_name("test-service");
418        let display = format!("{}", service_id);
419        assert!(display.starts_with("ServiceId(0x"));
420    }
421
422    #[test]
423    fn test_well_known_headers() {
424        // Well-known headers should match runtime-computed values
425        assert_eq!(*well_known::TRACE_ID, HeaderKeyId::from_name("trace_id"));
426        assert_eq!(*well_known::SPAN_ID, HeaderKeyId::from_name("span_id"));
427        assert_eq!(
428            *well_known::REQUEST_ID,
429            HeaderKeyId::from_name("request_id")
430        );
431        assert_eq!(
432            *well_known::CALLER_SERVICE,
433            HeaderKeyId::from_name("caller_service")
434        );
435        assert_eq!(
436            *well_known::IDEMPOTENCY_KEY,
437            HeaderKeyId::from_name("idempotency_key")
438        );
439
440        // They should all be distinct
441        assert_ne!(*well_known::TRACE_ID, *well_known::SPAN_ID);
442    }
443
444    #[test]
445    fn test_version_sensitivity() {
446        let v1 = InterfaceId::from_name("mensa.user.v1.UserInterface");
447        let v2 = InterfaceId::from_name("mensa.user.v2.UserInterface");
448        assert_ne!(v1, v2, "Different versions should have different IDs");
449    }
450
451    #[test]
452    fn test_ordering() {
453        let id1 = ServiceId::from_name("aaa");
454        let id2 = ServiceId::from_name("bbb");
455        let id3 = ServiceId::from_name("ccc");
456
457        // IDs should be orderable
458        let mut ids = [id3, id1, id2];
459        ids.sort();
460
461        // Ordering is based on hash, not name
462        assert_eq!(ids.len(), 3);
463    }
464}