Skip to main content

runcycles/models/
ids.rs

1//! Strongly-typed newtype wrappers for protocol identifiers.
2
3use serde::{Deserialize, Serialize};
4
5macro_rules! define_id {
6    ($(#[$meta:meta])* $name:ident) => {
7        $(#[$meta])*
8        #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
9        #[serde(transparent)]
10        pub struct $name(String);
11
12        impl $name {
13            /// Create a new identifier from any string-like value.
14            pub fn new(id: impl Into<String>) -> Self {
15                Self(id.into())
16            }
17
18            /// Borrow the inner string.
19            pub fn as_str(&self) -> &str {
20                &self.0
21            }
22
23            /// Consume and return the inner string.
24            pub fn into_inner(self) -> String {
25                self.0
26            }
27        }
28
29        impl std::fmt::Display for $name {
30            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31                f.write_str(&self.0)
32            }
33        }
34
35        impl From<String> for $name {
36            fn from(s: String) -> Self {
37                Self(s)
38            }
39        }
40
41        impl From<&str> for $name {
42            fn from(s: &str) -> Self {
43                Self(s.to_owned())
44            }
45        }
46    };
47}
48
49define_id!(
50    /// Unique identifier for a budget reservation.
51    ReservationId
52);
53
54define_id!(
55    /// Idempotency key for safe request retries.
56    IdempotencyKey
57);
58
59define_id!(
60    /// Unique identifier for a direct-debit event.
61    EventId
62);
63
64impl IdempotencyKey {
65    /// Generate a new random idempotency key (UUID v4).
66    pub fn random() -> Self {
67        Self(uuid::Uuid::new_v4().to_string())
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn newtype_display_and_serde() {
77        let id = ReservationId::new("rsv_123");
78        assert_eq!(id.as_str(), "rsv_123");
79        assert_eq!(id.to_string(), "rsv_123");
80
81        let json = serde_json::to_string(&id).unwrap();
82        assert_eq!(json, "\"rsv_123\"");
83
84        let deserialized: ReservationId = serde_json::from_str(&json).unwrap();
85        assert_eq!(deserialized, id);
86    }
87
88    #[test]
89    fn idempotency_key_random() {
90        let k1 = IdempotencyKey::random();
91        let k2 = IdempotencyKey::random();
92        assert_ne!(k1, k2);
93        assert_eq!(k1.as_str().len(), 36); // UUID v4 format
94    }
95
96    #[test]
97    fn from_conversions() {
98        let id: ReservationId = "rsv_abc".into();
99        assert_eq!(id.as_str(), "rsv_abc");
100
101        let id2: ReservationId = String::from("rsv_def").into();
102        assert_eq!(id2.as_str(), "rsv_def");
103    }
104
105    #[test]
106    fn into_inner() {
107        let id = ReservationId::new("rsv_xyz");
108        let inner: String = id.into_inner();
109        assert_eq!(inner, "rsv_xyz");
110
111        let ek = EventId::new("evt_123");
112        let inner2: String = ek.into_inner();
113        assert_eq!(inner2, "evt_123");
114    }
115}