unifly_api/model/
entity_id.rs1use serde::{Deserialize, Serialize};
8use std::error::Error as StdError;
9use std::fmt;
10use std::str::FromStr;
11use uuid::Uuid;
12
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
20#[serde(untagged)]
21pub enum EntityId {
22 Uuid(Uuid),
23 Legacy(String),
24}
25
26impl EntityId {
27 pub fn as_uuid(&self) -> Option<&Uuid> {
28 match self {
29 Self::Uuid(u) => Some(u),
30 Self::Legacy(_) => None,
31 }
32 }
33
34 pub fn as_legacy(&self) -> Option<&str> {
35 match self {
36 Self::Legacy(s) => Some(s),
37 Self::Uuid(_) => None,
38 }
39 }
40}
41
42impl fmt::Display for EntityId {
43 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44 match self {
45 Self::Uuid(u) => write!(f, "{u}"),
46 Self::Legacy(s) => write!(f, "{s}"),
47 }
48 }
49}
50
51impl FromStr for EntityId {
52 type Err = std::convert::Infallible;
53
54 fn from_str(s: &str) -> Result<Self, Self::Err> {
55 Ok(Self::from(s.to_owned()))
56 }
57}
58
59impl From<Uuid> for EntityId {
60 fn from(u: Uuid) -> Self {
61 Self::Uuid(u)
62 }
63}
64
65impl From<String> for EntityId {
66 fn from(s: String) -> Self {
67 match Uuid::parse_str(&s) {
68 Ok(u) => Self::Uuid(u),
69 Err(_) => Self::Legacy(s),
70 }
71 }
72}
73
74impl From<&str> for EntityId {
75 fn from(s: &str) -> Self {
76 Self::from(s.to_owned())
77 }
78}
79
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
84pub struct MacAddress(String);
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub struct ParseMacAddressError;
88
89impl MacAddress {
90 pub fn new(raw: impl AsRef<str>) -> Self {
95 let raw = raw.as_ref();
96 Self::try_new(raw).unwrap_or_else(|_| Self(raw.trim().to_lowercase().replace('-', ":")))
97 }
98
99 pub fn try_new(raw: impl AsRef<str>) -> Result<Self, ParseMacAddressError> {
100 normalize_mac(raw.as_ref())
101 .map(Self)
102 .ok_or(ParseMacAddressError)
103 }
104
105 pub fn as_str(&self) -> &str {
106 &self.0
107 }
108}
109
110fn normalize_mac(raw: &str) -> Option<String> {
111 let compact = raw
112 .trim()
113 .chars()
114 .filter(|ch| *ch != ':' && *ch != '-')
115 .collect::<String>()
116 .to_ascii_lowercase();
117
118 if compact.len() != 12 || !compact.chars().all(|ch| ch.is_ascii_hexdigit()) {
119 return None;
120 }
121
122 let bytes = compact.as_bytes();
123 let mut normalized = String::with_capacity(17);
124 for (idx, pair) in bytes.chunks_exact(2).enumerate() {
125 if idx > 0 {
126 normalized.push(':');
127 }
128 normalized.push(char::from(pair[0]));
129 normalized.push(char::from(pair[1]));
130 }
131
132 Some(normalized)
133}
134
135impl fmt::Display for MacAddress {
136 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137 write!(f, "{}", self.0)
138 }
139}
140
141impl fmt::Display for ParseMacAddressError {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "expected a 12-digit hexadecimal MAC address")
144 }
145}
146
147impl StdError for ParseMacAddressError {}
148
149impl FromStr for MacAddress {
150 type Err = ParseMacAddressError;
151
152 fn from_str(s: &str) -> Result<Self, Self::Err> {
153 Self::try_new(s)
154 }
155}
156
157#[cfg(test)]
158#[allow(clippy::unwrap_used)]
159mod tests {
160 use super::*;
161
162 #[test]
163 fn entity_id_from_uuid_string() {
164 let id = EntityId::from("550e8400-e29b-41d4-a716-446655440000".to_owned());
165 assert!(id.as_uuid().is_some());
166 }
167
168 #[test]
169 fn entity_id_from_legacy_string() {
170 let id = EntityId::from("507f1f77bcf86cd799439011".to_owned());
171 assert!(id.as_legacy().is_some());
172 }
173
174 #[test]
175 fn entity_id_display() {
176 let uuid = Uuid::parse_str("550e8400-e29b-41d4-a716-446655440000").unwrap();
177 let id = EntityId::Uuid(uuid);
178 assert_eq!(id.to_string(), "550e8400-e29b-41d4-a716-446655440000");
179 }
180
181 #[test]
182 fn entity_id_from_str() {
183 let id: EntityId = "507f1f77bcf86cd799439011".parse().unwrap();
184 assert!(id.as_legacy().is_some());
185 }
186
187 #[test]
188 fn mac_address_normalizes_dashes() {
189 let mac = MacAddress::new("AA-BB-CC-DD-EE-FF");
190 assert_eq!(mac.as_str(), "aa:bb:cc:dd:ee:ff");
191 }
192
193 #[test]
194 fn mac_address_normalizes_case() {
195 let mac = MacAddress::new("AA:BB:CC:DD:EE:FF");
196 assert_eq!(mac.as_str(), "aa:bb:cc:dd:ee:ff");
197 }
198
199 #[test]
200 fn mac_address_from_str() {
201 let mac: MacAddress = "AA-BB-CC-DD-EE-FF".parse().unwrap();
202 assert_eq!(mac.to_string(), "aa:bb:cc:dd:ee:ff");
203 }
204
205 #[test]
206 fn mac_address_normalizes_bare_hex() {
207 let mac = MacAddress::new("AABBCCDDEEFF");
208 assert_eq!(mac.as_str(), "aa:bb:cc:dd:ee:ff");
209 }
210
211 #[test]
212 fn mac_address_parse_rejects_invalid_input() {
213 assert!("not-a-mac".parse::<MacAddress>().is_err());
214 assert!("AA:BB:CC:DD:EE".parse::<MacAddress>().is_err());
215 }
216}