playdate_device/device/
serial.rs

1use std::borrow::Cow;
2use std::path::Path;
3use std::str::FromStr;
4use regex::Regex;
5
6
7/// Represents a device serial number.
8/// e.g.: PDU1-Y000235
9#[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
77// Commutative pares fore above
78impl 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}