playdate_device/device/
serial.rs1use std::borrow::Cow;
2use std::path::Path;
3use std::str::FromStr;
4use regex::Regex;
5
6
7#[derive(Clone)]
10pub struct SerialNumber(String);
11
12
13impl SerialNumber {
14 pub fn contained_in<S: AsRef<str>>(s: S) -> Option<Self> {
15 pub const REGEX_NAME: &str = r"^.*(PDU\d+[_-][a-zA-Z0-9]+).*$";
16 let re = Regex::new(REGEX_NAME).expect("invalid regex");
17 let captures = re.captures(s.as_ref())?;
18 let serial = Self::unify(captures.get(1)?.as_str());
19 let serial = if serial.contains('_') {
20 serial.replace('_', "-")
21 } else {
22 serial.to_string()
23 };
24
25 Some(Self(serial.to_owned()))
26 }
27
28
29 fn unify<'s, S: Into<Cow<'s, str>>>(s: S) -> Cow<'s, str> {
30 let s = s.into();
31 if s.contains('_') {
32 s.replace('_', "-").into()
33 } else {
34 s
35 }
36 }
37
38
39 pub fn as_str(&self) -> &str { &self.0 }
40}
41
42impl FromStr for SerialNumber {
43 type Err = error::SerialNumberFormatError;
44 fn from_str(s: &str) -> Result<Self, Self::Err> {
45 Self::contained_in(s).ok_or_else(|| error::SerialNumberFormatError::from(s))
46 }
47}
48
49
50impl TryFrom<String> for SerialNumber {
51 type Error = <Self as FromStr>::Err;
52 fn try_from(value: String) -> Result<Self, Self::Error> { Self::from_str(value.as_str()) }
53}
54
55impl TryFrom<&str> for SerialNumber {
56 type Error = <Self as FromStr>::Err;
57 fn try_from(value: &str) -> Result<Self, Self::Error> { Self::from_str(value) }
58}
59
60impl TryFrom<&Path> for SerialNumber {
61 type Error = <Self as FromStr>::Err;
62 fn try_from(value: &Path) -> Result<Self, Self::Error> { Self::from_str(value.to_string_lossy().as_ref()) }
63}
64
65
66impl PartialEq for SerialNumber {
67 fn eq(&self, other: &Self) -> bool { self.0.contains(&other.0) || other.0.contains(&self.0) }
68}
69
70impl<T: AsRef<str>> PartialEq<T> for SerialNumber {
71 fn eq(&self, other: &T) -> bool {
72 let other = other.as_ref().to_uppercase();
73 other.len() >= 3 && (self.0.contains(&other) || other.contains(&self.0))
74 }
75}
76
77impl PartialEq<SerialNumber> for &str {
79 fn eq(&self, sn: &SerialNumber) -> bool { sn.eq(self) }
80}
81impl PartialEq<SerialNumber> for String {
82 fn eq(&self, sn: &SerialNumber) -> bool { sn.eq(self) }
83}
84
85impl std::fmt::Debug for SerialNumber {
86 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
87 f.debug_tuple("Serial").field(&self.0).finish()
88 }
89}
90
91impl std::fmt::Display for SerialNumber {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) }
93}
94
95
96pub mod error {
97
98 use std::backtrace::Backtrace;
99 use thiserror::Error;
100 use miette::Diagnostic;
101
102
103 #[derive(Error, Debug, Diagnostic)]
104 #[error("invalid serial number `{value}`, expected format `PDUN-XNNNNNN`.")]
105 pub struct SerialNumberFormatError {
106 pub value: String,
107 #[backtrace]
108 backtrace: Backtrace,
109 }
110
111 impl SerialNumberFormatError {
112 fn new(value: String) -> Self {
113 Self { value,
114 backtrace: Backtrace::capture() }
115 }
116 }
117
118 impl From<String> for SerialNumberFormatError {
119 fn from(value: String) -> Self { Self::new(value) }
120 }
121
122 impl From<&str> for SerialNumberFormatError {
123 fn from(value: &str) -> Self { Self::new(value.to_owned()) }
124 }
125}
126
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 const SN: &str = "PDU0-X000042";
133 const SN_UNDERSCORE: &str = "PDU0_X000042";
134 const SN_FORMS: &[&str] = &[SN, SN_UNDERSCORE];
135
136 const PATHS: &[&str] = &["/dev/cu.usbmodem", "other/path/", "/", ""];
137
138 #[test]
139 fn from_str() {
140 let sn = SerialNumber::from_str(SN).unwrap();
141 let sn_ = SerialNumber::from_str(SN_UNDERSCORE).unwrap();
142 assert_eq!(sn, sn_);
143 assert_eq!(sn.0, sn_.0);
144 assert_eq!(sn.as_str(), sn_.as_str());
145 }
146
147 #[test]
148 fn from_port_path() {
149 const SUFFIX: &[Option<&str>] = &[None, Some("0"), Some("1"), Some("2"), Some("42")];
150
151 for sn in SN_FORMS {
152 for suffix in SUFFIX {
153 let suffix = suffix.unwrap_or_default();
154 for path in PATHS {
155 let path = format!("{path}{sn}{suffix}");
156 println!("parsing {path}");
157 let parsed = SerialNumber::from_str(&path).unwrap();
158 assert!(parsed == SN);
159 assert!(SN == parsed);
160 }
161 }
162 }
163 }
164
165 #[test]
166 fn from_port_path_nq() {
167 const SUFFIX: &[Option<&str>] = &[None, Some("0"), Some("1"), Some("2"), Some("42")];
168 let sn_forms: &[String] = &[SN.replace("42", "11"), SN_UNDERSCORE.replace("42", "11")];
169
170 for sn in sn_forms {
171 for suffix in SUFFIX {
172 let suffix = suffix.unwrap_or_default();
173 for path in PATHS {
174 let path = format!("{path}{sn}{suffix}");
175 println!("parsing {path}");
176 let parsed = SerialNumber::from_str(&path).unwrap();
177 assert!(parsed != SN);
178 assert!(SN != parsed);
179 }
180 }
181 }
182 }
183
184 #[test]
185 fn invalid() {
186 assert!(SerialNumber::from_str("").is_err());
187 assert!(SerialNumber::from_str("PDU").is_err());
188 assert!(SerialNumber::from_str("001").is_err());
189 assert!(SerialNumber::from_str("001-00000").is_err());
190 assert!(SerialNumber::from_str("PDU0--AAAAAAA").is_err());
191 }
192}