envelope_cli/models/
ids.rs

1//! Strongly-typed ID wrappers for all entity types
2//!
3//! Using newtype wrappers prevents accidentally mixing up IDs from different
4//! entity types at compile time.
5
6use serde::{Deserialize, Serialize};
7use std::fmt;
8use std::str::FromStr;
9use uuid::Uuid;
10
11/// Macro to generate ID newtype wrappers
12macro_rules! define_id {
13    ($name:ident, $display_prefix:literal) => {
14        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
15        #[serde(transparent)]
16        pub struct $name(Uuid);
17
18        impl $name {
19            /// Create a new random ID
20            pub fn new() -> Self {
21                Self(Uuid::new_v4())
22            }
23
24            /// Create an ID from an existing UUID
25            pub fn from_uuid(uuid: Uuid) -> Self {
26                Self(uuid)
27            }
28
29            /// Get the underlying UUID
30            pub fn as_uuid(&self) -> &Uuid {
31                &self.0
32            }
33
34            /// Parse an ID from a string
35            pub fn parse(s: &str) -> Result<Self, uuid::Error> {
36                Ok(Self(Uuid::parse_str(s)?))
37            }
38        }
39
40        impl Default for $name {
41            fn default() -> Self {
42                Self::new()
43            }
44        }
45
46        impl fmt::Display for $name {
47            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48                write!(f, "{}{}", $display_prefix, &self.0.to_string()[..8])
49            }
50        }
51
52        impl From<Uuid> for $name {
53            fn from(uuid: Uuid) -> Self {
54                Self(uuid)
55            }
56        }
57
58        impl FromStr for $name {
59            type Err = uuid::Error;
60
61            fn from_str(s: &str) -> Result<Self, Self::Err> {
62                // Try to parse the full UUID
63                if let Ok(uuid) = Uuid::parse_str(s) {
64                    return Ok(Self(uuid));
65                }
66                // Try stripping common prefixes
67                let s = s.strip_prefix($display_prefix).unwrap_or(s);
68                Ok(Self(Uuid::parse_str(s)?))
69            }
70        }
71    };
72}
73
74define_id!(AccountId, "acc-");
75define_id!(TransactionId, "txn-");
76define_id!(CategoryId, "cat-");
77define_id!(CategoryGroupId, "grp-");
78define_id!(PayeeId, "pay-");
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn test_account_id_creation() {
86        let id = AccountId::new();
87        assert!(!id.as_uuid().is_nil());
88    }
89
90    #[test]
91    fn test_id_display() {
92        let id = AccountId::new();
93        let display = format!("{}", id);
94        assert!(display.starts_with("acc-"));
95        assert_eq!(display.len(), 12); // "acc-" + 8 chars
96    }
97
98    #[test]
99    fn test_id_equality() {
100        let id1 = AccountId::new();
101        let id2 = id1;
102        assert_eq!(id1, id2);
103
104        let id3 = AccountId::new();
105        assert_ne!(id1, id3);
106    }
107
108    #[test]
109    fn test_id_serialization() {
110        let id = AccountId::new();
111        let json = serde_json::to_string(&id).unwrap();
112        let deserialized: AccountId = serde_json::from_str(&json).unwrap();
113        assert_eq!(id, deserialized);
114    }
115
116    #[test]
117    fn test_id_parse() {
118        let uuid_str = "550e8400-e29b-41d4-a716-446655440000";
119        let id = AccountId::parse(uuid_str).unwrap();
120        assert_eq!(id.as_uuid().to_string(), uuid_str);
121    }
122
123    #[test]
124    fn test_different_id_types_not_mixable() {
125        // This test documents that different ID types are distinct at compile time
126        let account_id = AccountId::new();
127        let transaction_id = TransactionId::new();
128
129        // These are different types - can't be compared directly
130        // This would fail to compile:
131        // assert_ne!(account_id, transaction_id);
132
133        // But we can compare their underlying UUIDs if needed
134        assert_ne!(account_id.as_uuid(), transaction_id.as_uuid());
135    }
136}