1use crate::error::{ModelError, Result};
10use serde::{Deserialize, Deserializer, Serialize, Serializer};
11
12pub(crate) const HAP_BASE_SUFFIX: &str = "-0000-1000-8000-0026BB765291";
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Uuid(String);
18
19impl Uuid {
20 pub fn parse(s: &str) -> Result<Self> {
26 let t = s.trim();
27 if t.len() == 36 && t.as_bytes()[8] == b'-' {
28 if t.chars().all(|c| c.is_ascii_hexdigit() || c == '-') {
30 return Ok(Uuid(t.to_ascii_lowercase()));
31 }
32 return Err(ModelError::MalformedUuid(s.to_string()));
33 }
34 if (1..=8).contains(&t.len()) && t.chars().all(|c| c.is_ascii_hexdigit()) {
36 let padded = format!("{:0>8}", t.to_ascii_lowercase());
37 return Ok(Uuid(
38 format!("{padded}{HAP_BASE_SUFFIX}").to_ascii_lowercase(),
39 ));
40 }
41 Err(ModelError::MalformedUuid(s.to_string()))
42 }
43
44 pub fn as_full(&self) -> &str {
46 &self.0
47 }
48
49 pub fn as_short(&self) -> Option<String> {
52 let suffix = HAP_BASE_SUFFIX.to_ascii_lowercase();
53 let head = self.0.strip_suffix(&suffix)?;
54 let trimmed = head.trim_start_matches('0');
56 let trimmed = if trimmed.is_empty() { "0" } else { trimmed };
57 Some(trimmed.to_ascii_uppercase())
58 }
59
60 pub(crate) fn from_full_unchecked(full: String) -> Self {
62 Uuid(full)
63 }
64}
65
66impl Serialize for Uuid {
67 fn serialize<S: Serializer>(&self, s: S) -> core::result::Result<S::Ok, S::Error> {
68 match self.as_short() {
70 Some(short) => s.serialize_str(&short),
71 None => s.serialize_str(&self.0),
72 }
73 }
74}
75
76impl<'de> Deserialize<'de> for Uuid {
77 fn deserialize<D: Deserializer<'de>>(d: D) -> core::result::Result<Self, D::Error> {
78 let raw = String::deserialize(d)?;
79 Uuid::parse(&raw).map_err(serde::de::Error::custom)
80 }
81}
82
83#[cfg(test)]
84#[allow(clippy::unwrap_used)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn short_3e_expands_to_accessory_information() {
91 let u = Uuid::parse("3E").unwrap();
92 assert_eq!(u.as_full(), "0000003e-0000-1000-8000-0026bb765291");
93 }
94
95 #[test]
96 fn short_43_expands_to_lightbulb() {
97 let u = Uuid::parse("43").unwrap();
98 assert_eq!(u.as_full(), "00000043-0000-1000-8000-0026bb765291");
99 }
100
101 #[test]
102 fn round_trips_short_form() {
103 let u = Uuid::parse("43").unwrap();
104 assert_eq!(u.as_short().as_deref(), Some("43"));
105 }
106
107 #[test]
108 fn full_vendor_uuid_round_trips_verbatim_and_has_no_short() {
109 let v = "00112233-4455-6677-8899-aabbccddeeff";
110 let u = Uuid::parse(v).unwrap();
111 assert_eq!(u.as_full(), v);
112 assert_eq!(u.as_short(), None);
113 }
114
115 #[test]
116 fn rejects_garbage() {
117 assert!(matches!(
118 Uuid::parse("zz"),
119 Err(ModelError::MalformedUuid(_))
120 ));
121 assert!(matches!(Uuid::parse(""), Err(ModelError::MalformedUuid(_))));
122 }
123}