Skip to main content

unifly_api/model/
entity_id.rs

1// ── Core identity types ──
2//
3// EntityId and MacAddress form the foundation of every domain type.
4// They unify UUID-based (Integration API) and string-based (Legacy API)
5// identifiers behind a single ergonomic interface.
6
7use serde::{Deserialize, Serialize};
8use std::fmt;
9use std::str::FromStr;
10use uuid::Uuid;
11
12// ── EntityId ────────────────────────────────────────────────────────
13
14/// Canonical identifier for any UniFi entity.
15///
16/// Transparently wraps either a UUID (Integration API) or a legacy
17/// MongoDB ObjectId string (Legacy API). Consumers never care which.
18#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[serde(untagged)]
20pub enum EntityId {
21    Uuid(Uuid),
22    Legacy(String),
23}
24
25impl EntityId {
26    pub fn as_uuid(&self) -> Option<&Uuid> {
27        match self {
28            Self::Uuid(u) => Some(u),
29            Self::Legacy(_) => None,
30        }
31    }
32
33    pub fn as_legacy(&self) -> Option<&str> {
34        match self {
35            Self::Legacy(s) => Some(s),
36            Self::Uuid(_) => None,
37        }
38    }
39}
40
41impl fmt::Display for EntityId {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        match self {
44            Self::Uuid(u) => write!(f, "{u}"),
45            Self::Legacy(s) => write!(f, "{s}"),
46        }
47    }
48}
49
50impl FromStr for EntityId {
51    type Err = std::convert::Infallible;
52
53    fn from_str(s: &str) -> Result<Self, Self::Err> {
54        Ok(Self::from(s.to_owned()))
55    }
56}
57
58impl From<Uuid> for EntityId {
59    fn from(u: Uuid) -> Self {
60        Self::Uuid(u)
61    }
62}
63
64impl From<String> for EntityId {
65    fn from(s: String) -> Self {
66        match Uuid::parse_str(&s) {
67            Ok(u) => Self::Uuid(u),
68            Err(_) => Self::Legacy(s),
69        }
70    }
71}
72
73impl From<&str> for EntityId {
74    fn from(s: &str) -> Self {
75        Self::from(s.to_owned())
76    }
77}
78
79// ── MacAddress ──────────────────────────────────────────────────────
80
81/// MAC address, normalized to lowercase colon-separated format (aa:bb:cc:dd:ee:ff).
82#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83pub struct MacAddress(String);
84
85impl MacAddress {
86    /// Create a normalized MAC address from any common format.
87    /// Accepts colon-separated, dash-separated, or bare hex.
88    pub fn new(raw: impl AsRef<str>) -> Self {
89        let normalized = raw.as_ref().to_lowercase().replace('-', ":");
90        Self(normalized)
91    }
92
93    pub fn as_str(&self) -> &str {
94        &self.0
95    }
96}
97
98impl fmt::Display for MacAddress {
99    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100        write!(f, "{}", self.0)
101    }
102}
103
104impl FromStr for MacAddress {
105    type Err = std::convert::Infallible;
106
107    fn from_str(s: &str) -> Result<Self, Self::Err> {
108        Ok(Self::new(s))
109    }
110}
111
112#[cfg(test)]
113#[allow(clippy::unwrap_used)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn entity_id_from_uuid_string() {
119        let id = EntityId::from("550e8400-e29b-41d4-a716-446655440000".to_owned());
120        assert!(id.as_uuid().is_some());
121    }
122
123    #[test]
124    fn entity_id_from_legacy_string() {
125        let id = EntityId::from("507f1f77bcf86cd799439011".to_owned());
126        assert!(id.as_legacy().is_some());
127    }
128
129    #[test]
130    fn entity_id_display() {
131        let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
132        let id = EntityId::Uuid(uuid);
133        assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
134    }
135
136    #[test]
137    fn entity_id_from_str() {
138        let id: EntityId = "507f1f77bcf86cd799439011".parse().unwrap();
139        assert!(id.as_legacy().is_some());
140    }
141
142    #[test]
143    fn mac_address_normalizes_dashes() {
144        let mac = MacAddress::new("AA-BB-CC-DD-EE-FF");
145        assert_eq!(mac.as_str(), "aa:bb:cc:dd:ee:ff");
146    }
147
148    #[test]
149    fn mac_address_normalizes_case() {
150        let mac = MacAddress::new("AA:BB:CC:DD:EE:FF");
151        assert_eq!(mac.as_str(), "aa:bb:cc:dd:ee:ff");
152    }
153
154    #[test]
155    fn mac_address_from_str() {
156        let mac: MacAddress = "AA-BB-CC-DD-EE-FF".parse().unwrap();
157        assert_eq!(mac.to_string(), "aa:bb:cc:dd:ee:ff");
158    }
159}